Compare commits

..
1 Commits
Author SHA1 Message Date
clawbot a3cc4e8369 Skip excluded paths before looking at their errors (closes #16)
check / check (push) Successful in 2m12s
filepath.Walk hands its callback the error for a path it could not
read, such as a directory it cannot list. Both callbacks, in
walkAndProcess and countFiles, returned that error before they checked
the exclusions, so a directory that --exclude or --exclude-dotfiles
excluded still failed the run when it could not be listed. Given an
error, each callback now checks the exclusions first and skips an
excluded path. The new test excludes a directory that cannot be listed
and runs sum add and check over its parent.

Model: opus-5-5
2026-10-06 03:58:49 +00:00
8 changed files with 52 additions and 102 deletions
+25 -33
View File
@@ -1,43 +1,35 @@
# Lint phase # Build stage
# golangci/golangci-lint:v2.12.2, 2026-10-05
FROM golangci/golangci-lint@sha256:5cceeef04e53efe1470638d4b4b4f5ceefd574955ab3941b2d9a68a8c9ad5240 AS lint
WORKDIR /src
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN golangci-lint run --config .golangci.yml ./...
# Test phase. -race needs cgo and so a C compiler, which the Debian Go
# image ships and the alpine one does not. The tests run as an
# unprivileged user: root can read a file with mode 0000, so the
# permission test would fail.
# golang:1.25.7-trixie, 2026-10-06
FROM golang@sha256:2b174ffcf56c7ad0c47d30d2630693265639ddf2a5141149c2da34db921791b4 AS test
RUN useradd --create-home testuser
USER testuser
WORKDIR /src
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN go test -timeout 90s -race -cover ./... || \
{ echo "--- Rerunning with -v for details ---"; \
go test -timeout 90s -race -v ./...; exit 1; }
# Build stage. Nothing is wanted from either phase above; the copies
# are what make BuildKit build them first, so this stage cannot run
# unless lint and test passed.
# golang 1.25-alpine, 2026-02-28 # golang 1.25-alpine, 2026-02-28
FROM golang@sha256:f6751d823c26342f9506c03797d2527668d095b0a15f1862cddb4d927a7a4ced AS builder FROM golang@sha256:f6751d823c26342f9506c03797d2527668d095b0a15f1862cddb4d927a7a4ced AS builder
COPY --from=lint /src/go.sum /dev/null
COPY --from=test /src/go.sum /dev/null RUN apk add --no-cache git make gcc musl-dev binutils-gold
RUN apk add --no-cache git make
# A tar-stream context keeps the sender's file owners, which git refuses. # golangci-lint v2.12.2, 2026-10-05
RUN git config --system --add safe.directory /src 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
WORKDIR /src WORKDIR /src
COPY go.mod go.sum ./ COPY go.mod go.sum ./
RUN go mod download RUN go mod download
COPY . . COPY . .
# Run the checks as an unprivileged user. Root bypasses file mode bits, which
# would make the permission tests (expecting EACCES on a 0000 file) spuriously
# pass with no error. Caches live under /tmp (world-writable) so the user needs
# no home directory of its own.
ENV GOCACHE=/tmp/gocache
ENV XDG_CACHE_HOME=/tmp/xdgcache
RUN adduser -D -u 1000 builder && chown -R builder:builder /src /go
USER builder
# Run all checks - build fails if any check fails
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 # The version stamped into the binary: the VERSION build argument when one
# is given, otherwise `git describe --tags --always` of the .git the build # 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 # context carries: the tag on a tagged commit, tag-N-gHASH on a commit after
+1 -3
View File
@@ -95,9 +95,7 @@ Future improvements under consideration:
* Author & maintainer: **sneak** – <sneak@sneak.berlin> * Author & maintainer: **sneak** – <sneak@sneak.berlin>
* Issues / PRs: <https://git.eeqj.de/sneak/attrsum/> * Issues / PRs: <https://git.eeqj.de/sneak/attrsum/>
* Code must pass `make check`, which runs the tests and golangci-lint as * Code must pass `go vet`, `go test ./...`, and `go fmt`.
phases of the `Dockerfile` (Docker is required) and checks formatting
with `gofmt`.
* No CLA; contributions are under WTFPL v2. * No CLA; contributions are under WTFPL v2.
--- ---
+8 -15
View File
@@ -17,22 +17,16 @@ have landed since the tag.
# Next Step # Next Step
Re-vendor the canonical files from `sneak/prompts` at `dd4027b` Policy scaffold commit: add LICENSE, REPO_POLICIES.md, .editorconfig,
(https://git.eeqj.de/sneak/attrsum/issues/13): add `REPO_POLICIES.md` .golangci.yml, and a comprehensive .gitignore (currently only the
and `.editorconfig`, refresh `.gitignore` (only the `attrsum` binary attrsum binary), and extend the Makefile (only test/build/clean/try
today), `.dockerignore`, `.gitea/workflows/check.yml` and today) with lint, fmt, fmt-check, check, and hooks targets.
`.golangci.yml`, and move the lint phase to golangci-lint v2.14.0.
# Completed Steps # Completed Steps
* 2026-10-06: a path that `--exclude` or `--exclude-dotfiles` excludes * 2026-10-06: a path that `--exclude` or `--exclude-dotfiles` excludes
is skipped even when it cannot be read, so an excluded directory is skipped even when it cannot be read, so an excluded directory
that cannot be listed no longer fails the run that cannot be listed no longer fails the run
* 2026-10-06: lint and test run as phases of the `Dockerfile`, and the
build stage depends on both; `script/lint` and `script/test` each
build their phase with `--no-cache`; `script/cibuild` bootstraps,
runs `script/check`, then builds the image; golangci-lint is no
longer installed on the host
* 2026-10-06: `check --continue` keeps going past a file or directory * 2026-10-06: `check --continue` keeps going past a file or directory
it cannot read: it counts it as failed, prints the error and the it cannot read: it counts it as failed, prints the error and the
path on stderr, and checks the rest of the tree path on stderr, and checks the rest of the tree
@@ -62,12 +56,11 @@ today), `.dockerignore`, `.gitea/workflows/check.yml` and
# Future Steps # Future Steps
* Add .gitea/workflows/check.yml
* Restructure README.md into the standard sections: Description, * Restructure README.md into the standard sections: Description,
Getting Started, Entrypoints, Rationale, Design, TODO, License, Getting Started, Rationale, Design, TODO, License, Author (Getting
Author (Getting Started, Why?, TODO, License exist; Description, Started, Why?, TODO, License exist; Description, Design, Author are
Entrypoints, Design, Author are missing) missing)
* Add a `LICENSE` file matching the README's WTFPL v2; sneak's to add,
not an agent's
* Tag a patch release to ship the 2026-02-02 correctness fixes * Tag a patch release to ship the 2026-02-02 correctness fixes
* Dry-run mode (--dry-run, -n): show what would be done without making * Dry-run mode (--dry-run, -n): show what would be done without making
changes (from README TODO) changes (from README TODO)
+7 -5
View File
@@ -3,14 +3,15 @@
# this repo. Idempotent: every install is guarded by a check so already # this repo. Idempotent: every install is guarded by a check so already
# 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.
# goimports is installed via `go install` at a pinned commit (never # golangci-lint and goimports are installed via `go install` at the same
# "latest"). The linter is not installed: it runs only as the lint phase # pinned commits the Dockerfile uses (never "latest").
# of the Dockerfile.
set -eu set -eu
ROOT="$(cd "$(dirname "$0")/.." && pwd -P)" ROOT="$(cd "$(dirname "$0")/.." && pwd -P)"
# Pinned versions, 2026-10-05 # 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 v0.42.0
GOIMPORTS_REF="golang.org/x/tools/cmd/goimports@009367f5c17a8d4c45a961a3a509277190a9a6f0" GOIMPORTS_REF="golang.org/x/tools/cmd/goimports@009367f5c17a8d4c45a961a3a509277190a9a6f0"
@@ -68,8 +69,9 @@ main() {
if missing make; then pkg_install gnumake make make make; fi if missing make; then pkg_install gnumake make make make; fi
if missing go; then pkg_install go golang go go; fi if missing go; then pkg_install go golang go go; fi
# Format tool, pinned via go install (installs into # Lint/format tools, pinned via go install (installs into
# "$(go env GOPATH)/bin"; ensure that is on your PATH). # "$(go env GOPATH)/bin"; ensure that is on your PATH).
if missing golangci-lint; then go install "$GOLANGCI_LINT_REF"; fi
if missing goimports; then go install "$GOIMPORTS_REF"; fi if missing goimports; then go install "$GOIMPORTS_REF"; fi
go mod download go mod download
+1 -3
View File
@@ -1,8 +1,6 @@
#!/bin/sh #!/bin/sh
# script/check: run all checks (test, lint, fmt-check). Our own # script/check: run all checks (test, lint, fmt-check). Our own
# extension to scripts-to-rule-them-all. test and lint are Docker # extension to scripts-to-rule-them-all. Must not modify any files.
# phases; fmt-check is native, because a formatter writes the working
# tree. Must not modify any files.
set -eu set -eu
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd -P)" SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd -P)"
+4 -19
View File
@@ -1,28 +1,13 @@
#!/bin/sh #!/bin/sh
# script/cibuild: run the CI build. It bootstraps first: a CI runner # script/cibuild: run the CI build. The Dockerfile runs make check, so
# checks out and runs this and nothing else, and script/fmt-check runs # a successful build implies all checks pass.
# the formatter on the host, which a pristine checkout cannot do.
# --no-cache for the same reason as script/docker: 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)" ROOT="$(cd "$(dirname "$0")/.." && pwd -P)"
ROOT="$(cd "$SCRIPT_DIR/.." && pwd -P)"
main() { main() {
cd "$ROOT" cd "$ROOT"
"$SCRIPT_DIR/bootstrap" docker build .
"$SCRIPT_DIR/check"
# 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 "$@" main "$@"
+3 -14
View File
@@ -1,23 +1,12 @@
#!/bin/sh #!/bin/sh
# script/lint: run the linter. Linting is a phase of the Dockerfile and # script/lint: run the linter.
# this builds that phase alone; the linter is never installed or run on
# a developer host, where a shared result cache and a host-global lock
# make its answer untrustworthy.
#
# The phase is not the last stage in the file, so it is built only when
# --target names it. --no-cache because a cached lint layer is a lint
# that did not run. The tag makes each build replace the previous image
# instead of leaving a dangling one behind.
set -eu set -eu
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd -P)" ROOT="$(cd "$(dirname "$0")/.." && pwd -P)"
ROOT="$(cd "$SCRIPT_DIR/.." && pwd -P)"
main() { main() {
cd "$ROOT" cd "$ROOT"
docker build --no-cache \ golangci-lint run --config .golangci.yml ./...
--target lint \
-t "$("$SCRIPT_DIR/projectname")-lint" .
} }
main "$@" main "$@"
+3 -10
View File
@@ -1,19 +1,12 @@
#!/bin/sh #!/bin/sh
# script/test: run the test suite. Testing is a phase of the Dockerfile # script/test: run the test suite.
# and this builds that phase alone, on the same terms as script/lint:
# --target because a phase that is not the last stage is built only when
# named, --no-cache because a cached test layer is a test that did not
# run, and a tag so each build replaces the previous image.
set -eu set -eu
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd -P)" ROOT="$(cd "$(dirname "$0")/.." && pwd -P)"
ROOT="$(cd "$SCRIPT_DIR/.." && pwd -P)"
main() { main() {
cd "$ROOT" cd "$ROOT"
docker build --no-cache \ go test -v -race -timeout 30s -cover ./...
--target test \
-t "$("$SCRIPT_DIR/projectname")-test" .
} }
main "$@" main "$@"