Format Markdown with prettier in make fmt and fmt-check (closes #23)
check / check (push) Successful in 2m19s

`make fmt` now runs prettier over the Markdown files after the Go
formatters, and `make fmt-check` fails when prettier would change one.
`package.json`, `yarn.lock`, `.prettierrc` and `.prettierignore` are the
`sneak/prompts` copies at `dd4027b`, except for `"private": true` in
`package.json` in place of the licence field. Both scripts find yarn the
way that repo's scripts do. `script/bootstrap` installs prettier with
`yarn install --frozen-lockfile`. When the yarn those scripts would run
is not 1.22.22, it installs yarn through corepack under node 22.17.0:
the node on `PATH` if it has that version, otherwise one installed
through nvm from a hash-checked archive. `README.md` and `TODO.md` are
reformatted once.

Model: opus-5-5
This commit was merged in pull request #25.
This commit is contained in:
2026-10-06 11:43:51 +02:00
parent 4666af1cc8
commit 8976a64832
9 changed files with 275 additions and 98 deletions
+2
View File
@@ -0,0 +1,2 @@
node_modules/
yarn.lock
+4
View File
@@ -0,0 +1,4 @@
{
"tabWidth": 4,
"proseWrap": "always"
}
+34 -28
View File
@@ -1,7 +1,7 @@
[**attrsum**](https://git.eeqj.de/sneak/attrsum/) is a **Go
1.22** command-line utility that **adds, updates, verifies, and clears per-file
file content checksums stored in extended attributes (xattrs) on macOS (APFS) and
Linux**, released under the [WTFPL v2](http://www.wtfpl.net/).
[**attrsum**](https://git.eeqj.de/sneak/attrsum/) is a **Go 1.22** command-line
utility that **adds, updates, verifies, and clears per-file file content
checksums stored in extended attributes (xattrs) on macOS (APFS) and Linux**,
released under the [WTFPL v2](http://www.wtfpl.net/).
Original release 2025-05-08.
@@ -53,30 +53,33 @@ find /data -name "*.jpg" | attrsum sum add -
attrsum -q sum add DIR
```
| xattr key | meaning |
|---------------------------------------------|--------------------------------|
| `user.berlin.sneak.app.attrsum.checksum` | base-58 multihash (sha2-256) |
| `user.berlin.sneak.app.attrsum.sumtime` | RFC 3339 timestamp of checksum |
| xattr key | meaning |
| ---------------------------------------- | ------------------------------ |
| `user.berlin.sneak.app.attrsum.checksum` | base-58 multihash (sha2-256) |
| `user.berlin.sneak.app.attrsum.sumtime` | RFC 3339 timestamp of checksum |
Flags:
* `-v, --verbose` — per-file log output
* `-q, --quiet` — suppress all output except errors (no progress bar or summary)
* `--exclude PATTERN` — skip paths matching rsync/Doublestar glob
* `--exclude-dotfiles` — skip any path component that starts with `.`
- `-v, --verbose` — per-file log output
- `-q, --quiet` — suppress all output except errors (no progress bar or summary)
- `--exclude PATTERN` — skip paths matching rsync/Doublestar glob
- `--exclude-dotfiles` — skip any path component that starts with `.`
All commands display a progress bar with ETA and print a summary report to stderr on completion (unless `--quiet` is specified).
All commands display a progress bar with ETA and print a summary report to
stderr on completion (unless `--quiet` is specified).
`attrsum` **never follows symlinks** and skips non-regular files (sockets, devices, …).
`attrsum` **never follows symlinks** and skips non-regular files (sockets,
devices, …).
---
## Why?
Apple APFS and Linux ext3/ext4 **store no per-file content checksums**, so
silent data corruption can pass unnoticed. `attrsum` keeps a portable checksum **inside each file’s xattrs**, providing integrity
verification that travels with the file itself—no external database
required. Now you can trust a USB stick didn't eat your data.
silent data corruption can pass unnoticed. `attrsum` keeps a portable checksum
**inside each file’s xattrs**, providing integrity verification that travels
with the file itself—no external database required. Now you can trust a USB
stick didn't eat your data.
---
@@ -84,21 +87,25 @@ required. Now you can trust a USB stick didn't eat your data.
Future improvements under consideration:
- **Dry-run mode (`--dry-run`, `-n`)** — show what would be done without making changes
- **JSON output (`--json`)** — machine-readable output for scripting and integration
- **Parallel processing (`-j N`)** — use multiple goroutines for faster checksumming on large trees
- **Dry-run mode (`--dry-run`, `-n`)** — show what would be done without making
changes
- **JSON output (`--json`)** — machine-readable output for scripting and
integration
- **Parallel processing (`-j N`)** — use multiple goroutines for faster
checksumming on large trees
- **Exit code documentation** — formalize and document exit codes for scripting
---
## Contributing
* Author & maintainer: **sneak** – <sneak@sneak.berlin>
* Issues / PRs: <https://git.eeqj.de/sneak/attrsum/>
* Code must pass `make check`, which runs the tests and golangci-lint as
phases of the `Dockerfile` (Docker is required) and checks formatting
with `gofmt -s` and goimports.
* No CLA; contributions are under WTFPL v2.
- Author & maintainer: **sneak** – <sneak@sneak.berlin>
- Issues / PRs: <https://git.eeqj.de/sneak/attrsum/>
- Code must pass `make check`, which runs the tests and golangci-lint as phases
of the `Dockerfile` (Docker is required) and checks formatting with `gofmt -s`
and goimports for Go and prettier for Markdown. `make bootstrap` installs
goimports and prettier, and `make fmt` fixes what the check reports.
- No CLA; contributions are under WTFPL v2.
---
@@ -112,5 +119,4 @@ No formal Code of Conduct; be excellent to each other.
## License
*Everything is permitted.*
See [WTFPL v2](http://www.wtfpl.net/txt/copying/).
_Everything is permitted._ See [WTFPL v2](http://www.wtfpl.net/txt/copying/).
+67 -69
View File
@@ -1,85 +1,83 @@
# Workflow
* branch (from `main`)
* do the work in Next Step
* move Next Step to the top of Completed Steps
* move the top item of Future Steps into Next Step
* commit (`TODO.md` changes in the same commit as the work)
* merge to `main` if the branch is not protected, otherwise open a PR
* push
- branch (from `main`)
- do the work in Next Step
- move Next Step to the top of Completed Steps
- move the top item of Future Steps into Next Step
- commit (`TODO.md` changes in the same commit as the work)
- merge to `main` if the branch is not protected, otherwise open a PR
- push
# Status
1.0+
Tagged 1.0.0 (2025-05-08). Substantial correctness fixes and features
have landed since the tag.
Tagged 1.0.0 (2025-05-08). Substantial correctness fixes and features have
landed since the tag.
# Next Step
Restructure README.md into the standard sections: Description, Getting
Started, Entrypoints, Rationale, Design, TODO, License, Author (Getting
Started, Why?, TODO, License exist; Description, Entrypoints, Design,
Author are missing)
Restructure README.md into the standard sections: Description, Getting Started,
Entrypoints, Rationale, Design, TODO, License, Author (Getting Started, Why?,
TODO, License exist; Description, Entrypoints, Design, Author are missing)
# Completed Steps
* 2026-10-06: `script/fmt-check` checks what `script/fmt` writes: it
fails and lists the files when `gofmt -s` or goimports would change
one, so a file that passes `make check` no longer changes on the
next `make fmt`
* 2026-10-06: canonical files re-vendored from `sneak/prompts` at
`dd4027b`: `REPO_POLICIES.md` and `.editorconfig` added;
`.dockerignore`, `.gitignore`, `.golangci.yml` and the workflow
refreshed, keeping this repo's own entries and `fetch-depth: 0`;
the lint phase runs golangci-lint v2.14.0; `script/bootstrap`
installs goimports unless the installed one has the pinned version,
and it and `script/fmt` put Go's bin directory on `PATH`
* 2026-10-06: the summary line after `sum`, `check` and `clear` prints
the byte unit once (`1.5 KiB`, not `1.5 KiB bytes`)
* 2026-10-06: a path that `--exclude` or `--exclude-dotfiles` excludes
is skipped even when it cannot be read, so an excluded directory
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
it cannot read: it counts it as failed, prints the error and the
path on stderr, and checks the rest of the tree
* 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
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
errors, single progress bar across paths, error on empty stdin,
dead code removal
* 2026-02-01: added quiet mode, progress bar, summary report, and stdin
path input; multiple file/directory arguments for all commands
* 2025-07-12: README update
* 2025-05-08: initial working tool with passing tests, skips
non-regular files, Makefile, README; tagged 1.0.0
- 2026-10-06: `make fmt` formats the Markdown files with prettier (four-space
indents, `proseWrap: always`) and `make fmt-check` fails on one it would
change; prettier is pinned in `package.json` and `yarn.lock`, and
`script/bootstrap` installs it, along with the pinned node and yarn when the
yarn on hand is not the pinned version; the Markdown files reformatted once
- 2026-10-06: `script/fmt-check` checks what `script/fmt` writes: it fails and
lists the files when `gofmt -s` or goimports would change one, so a file that
passes `make check` no longer changes on the next `make fmt`
- 2026-10-06: canonical files re-vendored from `sneak/prompts` at `dd4027b`:
`REPO_POLICIES.md` and `.editorconfig` added; `.dockerignore`, `.gitignore`,
`.golangci.yml` and the workflow refreshed, keeping this repo's own entries
and `fetch-depth: 0`; the lint phase runs golangci-lint v2.14.0;
`script/bootstrap` installs goimports unless the installed one has the pinned
version, and it and `script/fmt` put Go's bin directory on `PATH`
- 2026-10-06: the summary line after `sum`, `check` and `clear` prints the byte
unit once (`1.5 KiB`, not `1.5 KiB bytes`)
- 2026-10-06: a path that `--exclude` or `--exclude-dotfiles` excludes is
skipped even when it cannot be read, so an excluded directory 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 it cannot
read: it counts it as failed, prints the error and the path on stderr, and
checks the rest of the tree
- 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 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 errors, single progress bar across
paths, error on empty stdin, dead code removal
- 2026-02-01: added quiet mode, progress bar, summary report, and stdin path
input; multiple file/directory arguments for all commands
- 2025-07-12: README update
- 2025-05-08: initial working tool with passing tests, skips non-regular files,
Makefile, README; tagged 1.0.0
# Future Steps
* 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
* Dry-run mode (--dry-run, -n): show what would be done without making
changes (from README TODO)
* JSON output (--json) for scripting and integration (from README TODO)
* Parallel processing (-j N) with multiple goroutines for faster
checksumming on large trees (from README TODO)
* Formalize and document exit codes for scripting (from README TODO)
- 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
- Dry-run mode (--dry-run, -n): show what would be done without making changes
(from README TODO)
- JSON output (--json) for scripting and integration (from README TODO)
- Parallel processing (-j N) with multiple goroutines for faster checksumming on
large trees (from README TODO)
- Formalize and document exit codes for scripting (from README TODO)
+6
View File
@@ -0,0 +1,6 @@
{
"private": true,
"devDependencies": {
"prettier": "3.8.1"
}
}
+112 -1
View File
@@ -6,7 +6,11 @@
# goimports is installed via `go install` at a pinned commit (never
# "latest"), unless the installed one already has the pinned version.
# The linter is not installed: it runs only as the lint phase of the
# Dockerfile.
# Dockerfile. yarn is installed via corepack unless the yarn that
# script/fmt runs already has the pinned version. It is installed under
# the node on PATH if that has the pinned version; otherwise the pinned
# node is installed via nvm (installing nvm itself first, from a
# hash-verified release archive, never curl | sh).
set -eu
ROOT="$(cd "$(dirname "$0")/.." && pwd -P)"
@@ -16,6 +20,13 @@ ROOT="$(cd "$(dirname "$0")/.." && pwd -P)"
GOIMPORTS_VERSION="v0.42.0"
GOIMPORTS_REF="golang.org/x/tools/cmd/goimports@009367f5c17a8d4c45a961a3a509277190a9a6f0"
# Pinned versions, 2026-07-06
NODE_VERSION="22.17.0"
NVM_VERSION="0.40.3"
# sha256 of https://github.com/nvm-sh/nvm/archive/refs/tags/v0.40.3.tar.gz
NVM_SHA256="5f4d6aaa04a177dc93c985e31dbc411ab6b8c6e1e21d8015dbc1372625fcd1d0"
YARN_VERSION="1.22.22"
PKGMGR=""
SUDO=""
APT_UPDATED=""
@@ -87,6 +98,103 @@ ensure_goimports() {
echo "goimports $GOIMPORTS_VERSION installed"
}
# verify_sha256 <file> <expected-hash>
verify_sha256() {
if command -v sha256sum >/dev/null 2>&1; then
actual="$(sha256sum "$1" | cut -d' ' -f1)"
else
actual="$(shasum -a 256 "$1" | cut -d' ' -f1)"
fi
if [ "$actual" != "$2" ]; then
echo "bootstrap: sha256 mismatch for $1" >&2
echo " expected: $2" >&2
echo " actual: $actual" >&2
exit 1
fi
}
# nvm is a bash script; run a command in a bash with nvm loaded
nvm_sh() {
bash -c ". \"\$HOME/.nvm/nvm.sh\" && $*"
}
ensure_nvm() {
[ -s "$HOME/.nvm/nvm.sh" ] && return 0
# nvm prerequisites; nvm itself requires bash
if missing bash; then pkg_install bash bash bash bash; fi
if missing curl; then pkg_install curl curl curl curl; fi
if missing git; then pkg_install git git git git; fi
tmp="$(mktemp -d)"
curl -fsSL -o "$tmp/nvm.tar.gz" \
"https://github.com/nvm-sh/nvm/archive/refs/tags/v${NVM_VERSION}.tar.gz"
verify_sha256 "$tmp/nvm.tar.gz" "$NVM_SHA256"
mkdir -p "$HOME/.nvm"
tar -xzf "$tmp/nvm.tar.gz" -C "$HOME/.nvm" --strip-components=1
rm -rf "$tmp"
}
# Print the version of the node on PATH, such as v22.17.0, or nothing.
node_version() {
if missing node; then return 0; fi
node --version 2>/dev/null
}
ensure_node() {
if [ "$(node_version)" = "v$NODE_VERSION" ]; then
echo "node $NODE_VERSION already installed"
return 0
fi
ensure_nvm
nvm_sh "nvm install $NODE_VERSION"
echo "node $NODE_VERSION installed via nvm"
}
# Print the version of the yarn that script/fmt and script/fmt-check
# run, or nothing: the yarn on PATH, else the one under the pinned node
# in nvm.
yarn_version() {
if ! missing yarn; then
yarn --version 2>/dev/null
elif [ -s "$HOME/.nvm/nvm.sh" ]; then
nvm_sh "nvm use $NODE_VERSION >/dev/null && yarn --version" \
2>/dev/null
fi
}
# A yarn that already has the pinned version is used with the node that
# runs it. Otherwise yarn is installed via corepack under the pinned
# node.
ensure_yarn() {
if [ "$(yarn_version)" = "$YARN_VERSION" ]; then
echo "yarn $YARN_VERSION already installed"
return 0
fi
ensure_node
if [ "$(node_version)" = "v$NODE_VERSION" ]; then
corepack enable
corepack prepare "yarn@$YARN_VERSION" --activate
else
nvm_sh "nvm use $NODE_VERSION >/dev/null && corepack enable && \
corepack prepare yarn@$YARN_VERSION --activate"
fi
hash -r
if [ "$(yarn_version)" != "$YARN_VERSION" ]; then
echo "bootstrap: the yarn script/fmt runs is not $YARN_VERSION:" \
"$(command -v yarn || echo "none on PATH")" >&2
exit 1
fi
echo "yarn $YARN_VERSION installed"
}
install_js_deps() {
if missing yarn && [ -s "$HOME/.nvm/nvm.sh" ]; then
nvm_sh "nvm use $NODE_VERSION >/dev/null && cd \"$ROOT\" && \
yarn install --frozen-lockfile"
else
yarn install --frozen-lockfile
fi
}
main() {
cd "$ROOT"
@@ -102,6 +210,9 @@ main() {
go mod download
ensure_yarn
install_js_deps
echo "bootstrap complete"
}
+21
View File
@@ -4,6 +4,26 @@ set -eu
ROOT="$(cd "$(dirname "$0")/.." && pwd -P)"
# Must match the pin in script/bootstrap.
NODE_VERSION="22.17.0"
# script/bootstrap installs node and yarn under nvm and leaves neither
# on the PATH of the shell that called it, so resolve the pinned
# toolchain here the way bootstrap's own install step does. nvm is a
# bash script, hence the subshell.
run_yarn() {
if command -v yarn >/dev/null 2>&1; then
yarn "$@"
return
fi
if [ ! -s "$HOME/.nvm/nvm.sh" ]; then
echo "fmt: no yarn; run script/bootstrap first" >&2
exit 1
fi
bash -c '. "$HOME/.nvm/nvm.sh" && nvm use "$1" >/dev/null &&
shift && exec yarn "$@"' bash "$NODE_VERSION" "$@"
}
main() {
cd "$ROOT"
# script/bootstrap installs goimports into Go's bin directory, which
@@ -13,6 +33,7 @@ main() {
PATH="$gobin:$PATH"
gofmt -s -w .
goimports -w .
run_yarn run prettier --write '**/*.md' --tab-width 4 --prose-wrap always
}
main "$@"
+21
View File
@@ -5,6 +5,26 @@ set -eu
ROOT="$(cd "$(dirname "$0")/.." && pwd -P)"
# Must match the pin in script/bootstrap.
NODE_VERSION="22.17.0"
# script/bootstrap installs node and yarn under nvm and leaves neither
# on the PATH of the shell that called it, so resolve the pinned
# toolchain here the way bootstrap's own install step does. nvm is a
# bash script, hence the subshell.
run_yarn() {
if command -v yarn >/dev/null 2>&1; then
yarn "$@"
return
fi
if [ ! -s "$HOME/.nvm/nvm.sh" ]; then
echo "fmt-check: no yarn; run script/bootstrap first" >&2
exit 1
fi
bash -c '. "$HOME/.nvm/nvm.sh" && nvm use "$1" >/dev/null &&
shift && exec yarn "$@"' bash "$NODE_VERSION" "$@"
}
main() {
cd "$ROOT"
# script/bootstrap installs goimports into Go's bin directory, which
@@ -18,6 +38,7 @@ main() {
echo "$files" | sort -u >&2
exit 1
fi
run_yarn run prettier --check '**/*.md' --tab-width 4 --prose-wrap always
}
main "$@"
+8
View File
@@ -0,0 +1,8 @@
# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
# yarn lockfile v1
prettier@3.8.1:
version "3.8.1"
resolved "https://registry.yarnpkg.com/prettier/-/prettier-3.8.1.tgz#edf48977cf991558f4fcbd8a3ba6015ba2a3a173"
integrity sha512-UOnG6LftzbdaHZcKoPFtOcCKztrQ57WkHDeRD9t/PTQtmT0NHSeWWepj6pS0z/N7+08BHFDQVUrfmfMRcZwbMg==