2 Commits
Author SHA1 Message Date
clawbot 83bea41607 Record the gomodguard_v2 migration in TODO.md (closes #57)
check / check (push) Waiting to run
Adds the Completed Steps entry for re-vendoring the canonical
.golangci.yml, which switches off the deprecated gomodguard and runs
gomodguard_v2 and depguard in its place.

Model: opus-5-5
2026-10-04 13:16:17 +00:00
clawbot 8f2779a0cc Vendor the current canonical .golangci.yml
Fetched unchanged from the main branch of sneak/prompts. It switches
off the deprecated gomodguard, whose deprecation warning was printed on
every lint run, and turns on its successor gomodguard_v2 with the
shared module block list. It also turns on depguard with the rule that
keeps net/http/httptest out of files that are not tests. pixa has no
deny entries of its own to carry forward, and the tree needs no code
changes under the new linters.

Model: opus-5-5
2026-10-04 13:15:58 +00:00
4 changed files with 86 additions and 79 deletions
-4
View File
@@ -1,4 +0,0 @@
# Every PR adds an entry at the top of TODO.md's Completed Steps; union keeps
# both sides instead of conflicting. Git never reports a conflict here: read
# the merged entries after every merge or rebase.
TODO.md merge=union
+66 -2
View File
@@ -10,14 +10,20 @@ run:
linters: linters:
default: all 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: disable:
# Genuinely incompatible with project patterns # Genuinely incompatible with project patterns
- exhaustruct # Requires all struct fields - exhaustruct # Requires all struct fields
- depguard # Dependency allow/block lists
- godot # Requires comments to end with periods - godot # Requires comments to end with periods
- 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
# 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: settings:
lll: lll:
line-length: 88 line-length: 88
@@ -28,6 +34,64 @@ linters:
max-complexity: 15 max-complexity: 15
dupl: dupl:
threshold: 100 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: issues:
max-issues-per-linter: 0 max-issues-per-linter: 0
+5 -19
View File
@@ -3,8 +3,6 @@
* branch per issue from `next` * branch per issue from `next`
* 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
* `TODO.md` merges with git's union merge (`.gitattributes`), which never
reports a conflict: read the merged entries after every merge or rebase
* 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)
* open a PR based on `next` * open a PR based on `next`
@@ -31,23 +29,11 @@ P2: security: referer blacklist
# Completed Steps # Completed Steps
- 2026-10-04 `TODO.md` merges with git's union merge (closes #190): a root - 2026-10-04 `.golangci.yml` re-vendored from the canonical copy (closes #57):
`.gitattributes`, copied from `sneak/prompts`, marks it `merge=union`, so two the deprecated `gomodguard` is switched off, so lint runs print no
branches that each add an entry at the top of Completed Steps merge without a deprecation warning; its successor `gomodguard_v2` runs with the shared
conflict and keep both entries. Git now never reports a conflict in module block list, and `depguard` keeps `net/http/httptest` out of files that
`TODO.md`: a real one keeps both versions of the lines, and two entries that are not tests. The tree needed no code changes.
share an identical line can end up one inside the other, which a rebase can
do to an entry already on `next`. The Workflow above says to read the merged
entries after every merge or rebase.
- 2026-10-04 `TestEvictionRunsOnPeriodicSchedule` no longer races the evictor
(closes #183): it wrote each variant file and then inserted its accounting row
by hand, and a reconciliation pass between the two adopted the file first, so
the insert failed. It now writes the files only, while holding the test
database's only connection so the evictor's startup pass waits after walking
the empty variant directory; a periodic reconciliation pass then adopts the
files and the eviction pass after it evicts them. No other test in
`internal/imgcache` inserts a row by hand after starting the evictor. Test
only.
- 2026-10-04 deployment guide and example Caddy config (closes #89): - 2026-10-04 deployment guide and example Caddy config (closes #89):
"Deployment" in `README.md` says what the reverse proxy in front of pixa must "Deployment" in `README.md` says what the reverse proxy in front of pixa must
do (terminate TLS; pass `Host`, `Origin` and `Referer` on unchanged; set do (terminate TLS; pass `Host`, `Origin` and `Referer` on unchanged; set
+15 -54
View File
@@ -681,11 +681,6 @@ func TestEvictionRunsUnderWritePressure(t *testing.T) {
assertNoDanglingReferences(t, cache) assertNoDanglingReferences(t, cache)
} }
// TestEvictionRunsOnPeriodicSchedule writes three variant files straight
// to disk, bypassing StoreVariant, so they have no accounting rows and no
// write-pressure notification fires. Only a periodic reconciliation pass
// can then adopt them, and only the eviction pass that follows it can
// evict them.
func TestEvictionRunsOnPeriodicSchedule(t *testing.T) { func TestEvictionRunsOnPeriodicSchedule(t *testing.T) {
t.Parallel() t.Parallel()
@@ -693,29 +688,13 @@ func TestEvictionRunsOnPeriodicSchedule(t *testing.T) {
cache, _ := newEvictionTestCache(t, limit) cache, _ := newEvictionTestCache(t, limit)
// Hold the test database's only connection, so the startup pass // Start the evictor while the cache is empty, then create tracked
// waits for it after walking the still empty variant directory: the // over-limit state WITHOUT going through the store methods, so no
// files written while it waits are first seen by a periodic pass. // write-pressure notification fires and only the periodic ticker
conn, err := cache.db.Conn(t.Context()) // can trigger eviction.
if err != nil {
t.Fatalf("failed to take the database connection: %v", err)
}
defer func() { _ = conn.Close() }()
cache.StartEviction(100 * time.Millisecond) cache.StartEviction(100 * time.Millisecond)
defer func() { _ = cache.StopEviction(t.Context()) }() defer func() { _ = cache.StopEviction(t.Context()) }()
deadline := time.Now().Add(5 * time.Second)
for cache.db.Stats().WaitCount == 0 {
if time.Now().After(deadline) {
t.Fatal("the startup pass never waited for the database")
}
time.Sleep(10 * time.Millisecond)
}
keys := []VariantKey{ keys := []VariantKey{
testVariantKeyOne, testVariantKeyTwo, testVariantKeyThree, testVariantKeyOne, testVariantKeyTwo, testVariantKeyThree,
} }
@@ -724,43 +703,25 @@ func TestEvictionRunsOnPeriodicSchedule(t *testing.T) {
for i, key := range keys { for i, key := range keys {
content := bytes.Repeat([]byte{fills[i]}, 1000) content := bytes.Repeat([]byte{fills[i]}, 1000)
_, err = cache.variants.Store(key, bytes.NewReader(content), "image/webp") _, err := cache.variants.Store(key, bytes.NewReader(content), "image/webp")
if err != nil { if err != nil {
t.Fatalf("failed to store variant file: %v", err) t.Fatalf("failed to store variant file: %v", err)
} }
}
_ = conn.Close() _, err = cache.db.ExecContext(t.Context(),
`INSERT INTO variant_content (cache_key, size_bytes, content_type)
// Only one of the 1000-byte files fits under the limit: wait until VALUES (?, ?, ?)`,
// the evictor has removed the other two. string(key), len(content), "image/webp",
stored := len(keys) )
deadline = time.Now().Add(5 * time.Second) if err != nil {
t.Fatalf("failed to insert variant accounting row: %v", err)
for stored > 1 && time.Now().Before(deadline) {
time.Sleep(25 * time.Millisecond)
stored = 0
for _, key := range keys {
if cache.variants.Exists(key) {
stored++
}
} }
} }
if stored > 1 { usage := waitForUsageAtOrBelow(t, cache, limit, 5*time.Second)
t.Fatalf("periodic schedule did not trigger eviction: %d of %d "+
"variant files still on disk, want at most 1", stored, len(keys))
}
usage, err := cache.UsageBytes(t.Context())
if err != nil {
t.Fatalf("UsageBytes failed: %v", err)
}
if usage > limit { if usage > limit {
t.Errorf("usage after eviction = %d, want <= %d", usage, limit) t.Errorf("periodic schedule did not trigger eviction: usage = %d, want <= %d",
usage, limit)
} }
assertNoDanglingReferences(t, cache) assertNoDanglingReferences(t, cache)