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 [**attrsum**](https://git.eeqj.de/sneak/attrsum/) is a **Go 1.22** command-line
1.22** command-line utility that **adds, updates, verifies, and clears per-file utility that **adds, updates, verifies, and clears per-file file content
file content checksums stored in extended attributes (xattrs) on macOS (APFS) and checksums stored in extended attributes (xattrs) on macOS (APFS) and Linux**,
Linux**, released under the [WTFPL v2](http://www.wtfpl.net/). released under the [WTFPL v2](http://www.wtfpl.net/).
Original release 2025-05-08. Original release 2025-05-08.
@@ -53,30 +53,33 @@ find /data -name "*.jpg" | attrsum sum add -
attrsum -q sum add DIR attrsum -q sum add DIR
``` ```
| xattr key | meaning | | xattr key | meaning |
|---------------------------------------------|--------------------------------| | ---------------------------------------- | ------------------------------ |
| `user.berlin.sneak.app.attrsum.checksum` | base-58 multihash (sha2-256) | | `user.berlin.sneak.app.attrsum.checksum` | base-58 multihash (sha2-256) |
| `user.berlin.sneak.app.attrsum.sumtime` | RFC 3339 timestamp of checksum | | `user.berlin.sneak.app.attrsum.sumtime` | RFC 3339 timestamp of checksum |
Flags: Flags:
* `-v, --verbose` — per-file log output - `-v, --verbose` — per-file log output
* `-q, --quiet` — suppress all output except errors (no progress bar or summary) - `-q, --quiet` — suppress all output except errors (no progress bar or summary)
* `--exclude PATTERN` — skip paths matching rsync/Doublestar glob - `--exclude PATTERN` — skip paths matching rsync/Doublestar glob
* `--exclude-dotfiles` — skip any path component that starts with `.` - `--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? ## Why?
Apple APFS and Linux ext3/ext4 **store no per-file content checksums**, so 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 silent data corruption can pass unnoticed. `attrsum` keeps a portable checksum
verification that travels with the file itself—no external database **inside each file’s xattrs**, providing integrity verification that travels
required. Now you can trust a USB stick didn't eat your data. 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: Future improvements under consideration:
- **Dry-run mode (`--dry-run`, `-n`)** — show what would be done without making changes - **Dry-run mode (`--dry-run`, `-n`)** — show what would be done without making
- **JSON output (`--json`)** — machine-readable output for scripting and integration changes
- **Parallel processing (`-j N`)** — use multiple goroutines for faster checksumming on large trees - **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 - **Exit code documentation** — formalize and document exit codes for scripting
--- ---
## Contributing ## Contributing
* 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 `make check`, which runs the tests and golangci-lint as phases
phases of the `Dockerfile` (Docker is required) and checks formatting of the `Dockerfile` (Docker is required) and checks formatting with `gofmt -s`
with `gofmt -s` and goimports. and goimports for Go and prettier for Markdown. `make bootstrap` installs
* No CLA; contributions are under WTFPL v2. 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 ## License
*Everything is permitted.* _Everything is permitted._ See [WTFPL v2](http://www.wtfpl.net/txt/copying/).
See [WTFPL v2](http://www.wtfpl.net/txt/copying/).
+67 -69
View File
@@ -1,85 +1,83 @@
# Workflow # Workflow
* branch (from `main`) - branch (from `main`)
* do the work in Next Step - do the work in Next Step
* move Next Step to the top of Completed Steps - move Next Step to the top of Completed Steps
* move the top item of Future Steps into Next Step - move the top item of Future Steps into Next Step
* commit (`TODO.md` changes in the same commit as the work) - commit (`TODO.md` changes in the same commit as the work)
* merge to `main` if the branch is not protected, otherwise open a PR - merge to `main` if the branch is not protected, otherwise open a PR
* push - push
# Status # Status
1.0+ 1.0+
Tagged 1.0.0 (2025-05-08). Substantial correctness fixes and features Tagged 1.0.0 (2025-05-08). Substantial correctness fixes and features have
have landed since the tag. landed since the tag.
# Next Step # Next Step
Restructure README.md into the standard sections: Description, Getting Restructure README.md into the standard sections: Description, Getting Started,
Started, Entrypoints, Rationale, Design, TODO, License, Author (Getting Entrypoints, Rationale, Design, TODO, License, Author (Getting Started, Why?,
Started, Why?, TODO, License exist; Description, Entrypoints, Design, TODO, License exist; Description, Entrypoints, Design, Author are missing)
Author are missing)
# Completed Steps # Completed Steps
* 2026-10-06: `script/fmt-check` checks what `script/fmt` writes: it - 2026-10-06: `make fmt` formats the Markdown files with prettier (four-space
fails and lists the files when `gofmt -s` or goimports would change indents, `proseWrap: always`) and `make fmt-check` fails on one it would
one, so a file that passes `make check` no longer changes on the change; prettier is pinned in `package.json` and `yarn.lock`, and
next `make fmt` `script/bootstrap` installs it, along with the pinned node and yarn when the
* 2026-10-06: canonical files re-vendored from `sneak/prompts` at yarn on hand is not the pinned version; the Markdown files reformatted once
`dd4027b`: `REPO_POLICIES.md` and `.editorconfig` added; - 2026-10-06: `script/fmt-check` checks what `script/fmt` writes: it fails and
`.dockerignore`, `.gitignore`, `.golangci.yml` and the workflow lists the files when `gofmt -s` or goimports would change one, so a file that
refreshed, keeping this repo's own entries and `fetch-depth: 0`; passes `make check` no longer changes on the next `make fmt`
the lint phase runs golangci-lint v2.14.0; `script/bootstrap` - 2026-10-06: canonical files re-vendored from `sneak/prompts` at `dd4027b`:
installs goimports unless the installed one has the pinned version, `REPO_POLICIES.md` and `.editorconfig` added; `.dockerignore`, `.gitignore`,
and it and `script/fmt` put Go's bin directory on `PATH` `.golangci.yml` and the workflow refreshed, keeping this repo's own entries
* 2026-10-06: the summary line after `sum`, `check` and `clear` prints and `fetch-depth: 0`; the lint phase runs golangci-lint v2.14.0;
the byte unit once (`1.5 KiB`, not `1.5 KiB bytes`) `script/bootstrap` installs goimports unless the installed one has the pinned
* 2026-10-06: a path that `--exclude` or `--exclude-dotfiles` excludes version, and it and `script/fmt` put Go's bin directory on `PATH`
is skipped even when it cannot be read, so an excluded directory - 2026-10-06: the summary line after `sum`, `check` and `clear` prints the byte
that cannot be listed no longer fails the run unit once (`1.5 KiB`, not `1.5 KiB bytes`)
* 2026-10-06: lint and test run as phases of the `Dockerfile`, and the - 2026-10-06: a path that `--exclude` or `--exclude-dotfiles` excludes is
build stage depends on both; `script/lint` and `script/test` each skipped even when it cannot be read, so an excluded directory that cannot be
build their phase with `--no-cache`; `script/cibuild` bootstraps, listed no longer fails the run
runs `script/check`, then builds the image; golangci-lint is no - 2026-10-06: lint and test run as phases of the `Dockerfile`, and the build
longer installed on the host stage depends on both; `script/lint` and `script/test` each build their phase
* 2026-10-06: `check --continue` keeps going past a file or directory with `--no-cache`; `script/cibuild` bootstraps, runs `script/check`, then
it cannot read: it counts it as failed, prints the error and the builds the image; golangci-lint is no longer installed on the host
path on stderr, and checks the rest of the tree - 2026-10-06: `check --continue` keeps going past a file or directory it cannot
* 2026-10-05: golangci-lint settings take effect: canonical read: it counts it as failed, prints the error and the path on stderr, and
`.golangci.yml` (v2 layout, settings under `linters.settings`), checks the rest of the tree
golangci-lint pinned at v2.12.2 in `Dockerfile` and - 2026-10-05: golangci-lint settings take effect: canonical `.golangci.yml` (v2
`script/bootstrap`, and the code fixed for what the settings now layout, settings under `linters.settings`), golangci-lint pinned at v2.12.2 in
report (long lines in `attrsum.go` rewrapped) `Dockerfile` and `script/bootstrap`, and the code fixed for what the settings
* 2026-10-05: `make try` runs on three small files in a temporary now report (long lines in `attrsum.go` rewrapped)
directory that it removes afterwards, also when a step fails, - 2026-10-05: `make try` runs on three small files in a temporary directory that
instead of on a fixed directory on one person's machine it removes afterwards, also when a step fails, instead of on a fixed directory
* 2026-10-02: `attrsum --version` reports the git tag or short commit, on one person's machine
stamped by `make build` and by a plain `docker build .` of a clone; - 2026-10-02: `attrsum --version` reports the git tag or short commit, stamped
`.dockerignore` sends `.git` without `.git/config` and keeps a by `make build` and by a plain `docker build .` of a clone; `.dockerignore`
host-built `attrsum` out; CI checks out full history so it stamps sends `.git` without `.git/config` and keeps a host-built `attrsum` out; CI
the same version checks out full history so it stamps the same version
* 2026-02-02: correctness pass: track actual bytes read instead of - 2026-02-02: correctness pass: track actual bytes read instead of stale file
stale file size, atomic failure tracking in ProcessCheck, detect size, atomic failure tracking in ProcessCheck, detect file modification during
file modification during checksum (TOCTOU), propagate countFiles checksum (TOCTOU), propagate countFiles errors, single progress bar across
errors, single progress bar across paths, error on empty stdin, paths, error on empty stdin, dead code removal
dead code removal - 2026-02-01: added quiet mode, progress bar, summary report, and stdin path
* 2026-02-01: added quiet mode, progress bar, summary report, and stdin input; multiple file/directory arguments for all commands
path input; multiple file/directory arguments for all commands - 2025-07-12: README update
* 2025-07-12: README update - 2025-05-08: initial working tool with passing tests, skips non-regular files,
* 2025-05-08: initial working tool with passing tests, skips Makefile, README; tagged 1.0.0
non-regular files, Makefile, README; tagged 1.0.0
# Future Steps # Future Steps
* Add a `LICENSE` file matching the README's WTFPL v2; sneak's to add, - Add a `LICENSE` file matching the README's WTFPL v2; sneak's to add, not an
not an agent's 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
changes (from README TODO) (from README TODO)
* JSON output (--json) for scripting and integration (from README TODO) - JSON output (--json) for scripting and integration (from README TODO)
* Parallel processing (-j N) with multiple goroutines for faster - Parallel processing (-j N) with multiple goroutines for faster checksumming on
checksumming on large trees (from README TODO) large trees (from README TODO)
* Formalize and document exit codes for scripting (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 # goimports is installed via `go install` at a pinned commit (never
# "latest"), unless the installed one already has the pinned version. # "latest"), unless the installed one already has the pinned version.
# The linter is not installed: it runs only as the lint phase of the # 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 set -eu
ROOT="$(cd "$(dirname "$0")/.." && pwd -P)" ROOT="$(cd "$(dirname "$0")/.." && pwd -P)"
@@ -16,6 +20,13 @@ ROOT="$(cd "$(dirname "$0")/.." && pwd -P)"
GOIMPORTS_VERSION="v0.42.0" GOIMPORTS_VERSION="v0.42.0"
GOIMPORTS_REF="golang.org/x/tools/cmd/goimports@009367f5c17a8d4c45a961a3a509277190a9a6f0" 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="" PKGMGR=""
SUDO="" SUDO=""
APT_UPDATED="" APT_UPDATED=""
@@ -87,6 +98,103 @@ ensure_goimports() {
echo "goimports $GOIMPORTS_VERSION installed" 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() { main() {
cd "$ROOT" cd "$ROOT"
@@ -102,6 +210,9 @@ main() {
go mod download go mod download
ensure_yarn
install_js_deps
echo "bootstrap complete" echo "bootstrap complete"
} }
+21
View File
@@ -4,6 +4,26 @@ set -eu
ROOT="$(cd "$(dirname "$0")/.." && pwd -P)" 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() { main() {
cd "$ROOT" cd "$ROOT"
# script/bootstrap installs goimports into Go's bin directory, which # script/bootstrap installs goimports into Go's bin directory, which
@@ -13,6 +33,7 @@ main() {
PATH="$gobin:$PATH" PATH="$gobin:$PATH"
gofmt -s -w . gofmt -s -w .
goimports -w . goimports -w .
run_yarn run prettier --write '**/*.md' --tab-width 4 --prose-wrap always
} }
main "$@" main "$@"
+21
View File
@@ -5,6 +5,26 @@ set -eu
ROOT="$(cd "$(dirname "$0")/.." && pwd -P)" 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() { main() {
cd "$ROOT" cd "$ROOT"
# script/bootstrap installs goimports into Go's bin directory, which # script/bootstrap installs goimports into Go's bin directory, which
@@ -18,6 +38,7 @@ main() {
echo "$files" | sort -u >&2 echo "$files" | sort -u >&2
exit 1 exit 1
fi fi
run_yarn run prettier --check '**/*.md' --tab-width 4 --prose-wrap always
} }
main "$@" 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==