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
9 changed files with 109 additions and 185 deletions
-63
View File
@@ -1,63 +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-*
# This repo's host-built binary (`make build`); the image builds its own.
/attrsum
-4
View File
@@ -6,8 +6,4 @@ jobs:
steps: steps:
# actions/checkout v4.2.2, 2026-02-28 # actions/checkout v4.2.2, 2026-02-28
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 - 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 - run: script/cibuild
+14 -12
View File
@@ -1,5 +1,9 @@
version: "2" 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: run:
timeout: 5m timeout: 5m
modules-download-mode: readonly modules-download-mode: readonly
@@ -14,19 +18,17 @@ linters:
- wsl # Deprecated, replaced by wsl_v5 - wsl # Deprecated, replaced by wsl_v5
- wrapcheck # Too verbose for internal packages - wrapcheck # Too verbose for internal packages
- varnamelen # Short names like db, id are idiomatic Go - varnamelen # Short names like db, id are idiomatic Go
settings:
linters-settings: lll:
lll: line-length: 88
line-length: 88 funlen:
funlen: lines: 80
lines: 80 statements: 50
statements: 50 cyclop:
cyclop: max-complexity: 15
max-complexity: 15 dupl:
dupl: threshold: 100
threshold: 100
issues: issues:
exclude-use-default: false
max-issues-per-linter: 0 max-issues-per-linter: 0
max-same-issues: 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 RUN apk add --no-cache git make gcc musl-dev binutils-gold
# golangci-lint v2.10.1 # golangci-lint v2.12.2, 2026-08-07
RUN go install github.com/golangci/golangci-lint/v2/cmd/golangci-lint@5d1e709b7be35cb2025444e19de266b056b7b7ee RUN go install github.com/golangci/golangci-lint/v2/cmd/golangci-lint@v2.12.2
# goimports v0.42.0 # goimports v0.42.0
RUN go install golang.org/x/tools/cmd/goimports@009367f5c17a8d4c45a961a3a509277190a9a6f0 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 # Build the binary (still as the unprivileged user: it owns /src, so git VCS
# stamping sees consistent ownership). # 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 "no version could be derived although the build context carries .git" >&2; \
exit 1; \
fi; \
make build VERSION="${version:-dev}"
# Runtime stage # Runtime stage
# alpine 3.21, 2026-02-28 # alpine 3.21, 2026-02-28
+1 -6
View File
@@ -2,11 +2,6 @@
TESTDIR := $(HOME)/Documents/_SYSADMIN/cyberdyne 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/ # Standard targets are thin shims; the implementations live in script/
# per the scripts-to-rule-them-all pattern. # per the scripts-to-rule-them-all pattern.
@@ -40,7 +35,7 @@ hooks:
@script/install-precommit @script/install-precommit
build: clean build: clean
@go build -ldflags "-X main.Version=$(VERSION)" . @go build .
clean: clean:
@rm -f attrsum @rm -f attrsum
+5 -5
View File
@@ -26,11 +26,11 @@ $(HOME)/Documents/_SYSADMIN/cyberdyne path.
# Completed Steps # Completed Steps
* 2026-10-02: `attrsum --version` reports the git tag or short commit, * 2026-08-07: updated golangci-lint to v2.12.2: canonical
stamped by `make build` and by a plain `docker build .` of a clone; `.golangci.yml` (settings moved under `linters.settings` so the
`.dockerignore` sends `.git` without `.git/config` and keeps a configured thresholds actually apply), version pins bumped in
host-built `attrsum` out; CI checks out full history so it stamps `Dockerfile` and `script/bootstrap`, and long lines wrapped to
the same version satisfy the now-effective `lll` limit of 88
* 2026-02-02: correctness pass: track actual bytes read instead of * 2026-02-02: correctness pass: track actual bytes read instead of
stale file size, atomic failure tracking in ProcessCheck, detect stale file size, atomic failure tracking in ProcessCheck, detect
file modification during checksum (TOCTOU), propagate countFiles file modification during checksum (TOCTOU), propagate countFiles
+81 -63
View File
@@ -36,10 +36,6 @@ const (
progressThrottle = 250 * time.Millisecond 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. // Sentinel errors returned by the command implementations.
var ( var (
errNoPaths = errors.New("no paths provided") errNoPaths = errors.New("no paths provided")
@@ -76,7 +72,8 @@ func (s *Stats) Print(opts *options, operation string) {
return 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, operation,
s.FilesProcessed, s.FilesProcessed,
s.FilesSkipped, s.FilesSkipped,
@@ -105,9 +102,8 @@ func main() {
opts := &options{} opts := &options{}
rootCmd := &cobra.Command{ rootCmd := &cobra.Command{
Use: "attrsum", Use: "attrsum",
Short: "Compute and verify file checksums via xattrs", Short: "Compute and verify file checksums via xattrs",
Version: Version,
} }
rootCmd.SilenceUsage = true rootCmd.SilenceUsage = true
rootCmd.SilenceErrors = 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. // 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 // 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. // 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) total, err := countFilesMultiple(opts, paths)
if err != nil { if err != nil {
return nil, err return nil, err
@@ -193,7 +193,9 @@ func finishBar(bar *progressbar.ProgressBar) {
// runOverPaths runs process over each path, sharing the progress/stats // runOverPaths runs process over each path, sharing the progress/stats
// bookkeeping common to the sum-add, sum-update and clear commands. // 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) paths, err := expandPaths(args)
if err != nil { if err != nil {
return err return err
@@ -258,46 +260,54 @@ func newSumCmd(opts *options) *cobra.Command {
return cmd return cmd
} }
func processSumAdd(opts *options, dir string, stats *Stats, bar *progressbar.ProgressBar) error { func processSumAdd(
return walkAndProcess(opts, dir, stats, bar, func(p string, info os.FileInfo, s *Stats) error { opts *options, dir string, stats *Stats, bar *progressbar.ProgressBar,
if hasXattr(p, checksumKey) { ) error {
atomic.AddInt64(&s.FilesSkipped, 1) 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) err := writeChecksumAndTime(opts, p, info, s)
if err != nil { 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 {
atomic.AddInt64(&s.FilesFailed, 1) atomic.AddInt64(&s.FilesFailed, 1)
return werr return err
} }
return nil 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. // Record mtime before hashing to detect modifications during hash.
mtimeBefore := info.ModTime() 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 { func processClear(
return walkAndProcess(opts, dir, stats, bar, func(p string, info os.FileInfo, s *Stats) error { opts *options, dir string, stats *Stats, bar *progressbar.ProgressBar,
cleared, err := clearOne(opts, p) ) error {
if err != nil { return walkAndProcess(opts, dir, stats, bar,
atomic.AddInt64(&s.FilesFailed, 1) 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 { if cleared {
atomic.AddInt64(&s.FilesProcessed, 1) atomic.AddInt64(&s.FilesProcessed, 1)
atomic.AddInt64(&s.BytesProcessed, info.Size()) atomic.AddInt64(&s.BytesProcessed, info.Size())
} else { } else {
atomic.AddInt64(&s.FilesSkipped, 1) atomic.AddInt64(&s.FilesSkipped, 1)
} }
return nil return nil
}) })
} }
// clearOne removes both checksum xattrs from a single path, reporting // 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) 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 return cmd
} }
@@ -477,13 +491,16 @@ func runCheck(opts *options, args []string, cont bool) error {
return finalErr 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. // Track initial failed count to detect failures during this walk.
initialFailed := atomic.LoadInt64(&stats.FilesFailed) initialFailed := atomic.LoadInt64(&stats.FilesFailed)
err := walkAndProcess(opts, dir, stats, bar, func(p string, _ os.FileInfo, s *Stats) error { err := walkAndProcess(opts, dir, stats, bar,
return checkOne(opts, p, cont, s) func(p string, _ os.FileInfo, s *Stats) error {
}) return checkOne(opts, p, cont, s)
})
if err != nil { if err != nil {
if errors.Is(err, errVerification) { if errors.Is(err, errVerification) {
return errVerification return errVerification
@@ -747,7 +764,8 @@ func hasXattr(path, key string) bool {
func fileMultihash(path string) ([]byte, int64, error) { func fileMultihash(path string) ([]byte, int64, error) {
// The path is supplied by the operator as the tree to checksum; reading // The path is supplied by the operator as the tree to checksum; reading
// it is the entire purpose of the tool. // 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 { if err != nil {
return nil, 0, err return nil, 0, err
} }
+4 -4
View File
@@ -4,14 +4,14 @@
# installed tools are skipped. Base tooling comes from nix, apt, brew, # installed tools are skipped. Base tooling comes from nix, apt, brew,
# or apk (detected in that order); assumes nothing is present. # or apk (detected in that order); assumes nothing is present.
# golangci-lint and goimports are installed via `go install` at the same # 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 set -eu
ROOT="$(cd "$(dirname "$0")/.." && pwd -P)" ROOT="$(cd "$(dirname "$0")/.." && pwd -P)"
# Pinned versions, 2026-07-07 (same pins as the Dockerfile) # Pinned versions, 2026-08-07 (same pins as the Dockerfile)
# golangci-lint v2.10.1 # golangci-lint v2.12.2
GOLANGCI_LINT_REF="github.com/golangci/golangci-lint/v2/cmd/golangci-lint@5d1e709b7be35cb2025444e19de266b056b7b7ee" GOLANGCI_LINT_REF="github.com/golangci/golangci-lint/v2/cmd/golangci-lint@v2.12.2"
# goimports v0.42.0 # goimports v0.42.0
GOIMPORTS_REF="golang.org/x/tools/cmd/goimports@009367f5c17a8d4c45a961a3a509277190a9a6f0" GOIMPORTS_REF="golang.org/x/tools/cmd/goimports@009367f5c17a8d4c45a961a3a509277190a9a6f0"
+1 -11
View File
@@ -1,8 +1,6 @@
#!/bin/sh #!/bin/sh
# script/docker: build the Docker image tagged with the project name. # script/docker: build the Docker image tagged with the project name.
# Identical in all repos; the tag comes from script/projectname. # 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 set -eu
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd -P)" SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd -P)"
@@ -10,15 +8,7 @@ ROOT="$(cd "$SCRIPT_DIR/.." && pwd -P)"
main() { main() {
cd "$ROOT" cd "$ROOT"
# Own line: a failing command substitution inside an argument does docker build -t "$("$SCRIPT_DIR/projectname")" .
# 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 "$@" main "$@"