Correct release-blocking README and startup-warning inaccuracies (closes #151) #156
Reference in New Issue
Block a user
Delete Branch "issue-151-release-accuracy"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Closes #151.
Rework of the review at
#156 (comment). All
fifteen findings addressed, plus the two the reviewer flagged as minor,
plus the open
/s/*question, plus a re-sweep of the sections therebase moved.
Discipline note on the claim table below
The previous claim table asserted "correct, left alone" for statements
that are false in the code (findings 1, 5, 7, 11). That is worse than
no table, because it launders unchecked statements as checked. Every
row in this revision was re-derived by opening the named file this
time. Rows are marked REVISITED where the earlier verdict was wrong,
and the earlier verdict is shown so the error is visible rather than
quietly overwritten.
The eight items of the issue — unchanged, all still passing
Items 1 through 8 are as reviewed and were not touched:
warnSharedRateLimitBucketno longer gated onIsProd(); fireswhenever
TRUSTED_PROXIESis empty, in any environment. Keying,limits and the
TRUSTED_PROXIESdefault untouched(#150 is not mine).
TestSharedRateLimitBucketWarningruns all four cases.POSTonly, 405 withAllow: POST.TRUSTED_PROXIEScondition.#### Sessionsheading added.sessionIdleTimeoutadded to the startup configuration summary.TODO.mdFuture Steps splits change from reset.TODO.mdStatus claims only the cache-defeated container runs.The fifteen findings
README.md:892-894target_slack.go:37embeds*httpCore;:84-86passesd.Target.MaxRetriesinto the shareddeliver;target_http.goroutesmaxRetries != 0towithRetry, which takes a breakermax_retriestable row, thecircuit_breaker.gotree comment all now sayhttpandslack. Also added the missingslackbullet to the target-type list, which named only three of the four types the table above it declaresREADME.md:1271-1273script/lintrunsgolangci-linton the host; onlyscript/cibuildandscript/dockeruse DockerREADME.md:669,680file:%s?cache=shared&mode=rwc(webhook_db_manager.go:237,database.go:140); grep forPRAGMA/journal_modeover--include=*.goreturns only those two DSN lines and test DSNsjournal_modepragma is issued, so it is the default rollback journal. Per-file page cache and lock kept — those are realREADME.md:738webhook_urlSlackTargetConfig.WebhookURLisjson:"webhookUrl"(target_slack.go:27); handler writeswebhookUrl(source_management.go:1095).webhook_urlis only the error message text at:21webhookUrl, with a note that the underscored spelling is the message and not the keyREADME.md:624,1244BaseModel" / "soft deletes on all entities"model_setting.gohas onlyKeyandValueSettingexplicitly; the README's own Setting table already said soREADME.md:761,797DeliveryTaskdelivery.Task(engine.go);DeliveryTaskappears nowheredelivery.TaskREADME.md:29# Run all checks (format, lint, test, build)script/checkruns test, lint, fmt-checkREADME.md:495/webhook/{uuid}"Path: uuid.New().String()(source_management.go:327,:931), matched against the raw chi param (webhook.go:124-126)README.md:520max_queue_size"Maximum queued deliveries"target_config_view.go:158-161; nothing in the engine consults itREADME.md:762,792len(body) < MaxInlineBodySize(webhook.go:244,engine.go:1054)README.md:4670in the finite bandBeforeSaverewrites<= 0toRetentionForeverDays(model_webhook.go);parseRetentionDaysrejectsv < 0outright1..106751;0moved to the retain-forever band with the mechanism named; negatives called out as a 400, which the three-band text did not cover at allREADME.md:909httprate.KeyByEndpoint(ratelimit.go:303-306) — per client IP per request pathREADME.md:820-821WaitGroup.Wait()ensures clean shutdown"engine.go:302callslifecycle.WaitForShutdown, bounded by fx's stop context; on timeout it logs ERROR, returns an error, and goroutines keep runningREADME.md:1116and:261-262script/" —:1116added by this PRbuild,run,dev,deps,clean,cssare inlineREADME.md:372model_*.gofilesThe two the reviewer called minor
internal/{database,middleware,session}/testing.go.My call: added, not defended as deliberate. They are ordinary
compiled sources, not
_test.goscaffolding, and they export API(
NewTestDatabase,NewForTest) visible to any importer of thepackage. The tree lists every other non-test source file in every
other package, and nothing in the section states an exclusion, so
the absence reads as an oversight rather than a policy. Verified
complete in both directions against
git ls-files, which alsoturned up
internal/handlers/event_log_view.go, absent from thetree since #135 landed it
on
next; added too.README.md:1180"counted in the metrics" — the Metrics middlewareis only registered when
METRICS_USERNAMEis set(
routes.go:38-40), and Sentry only whenSENTRY_DSNis. Reworded:logged and given security headers unconditionally, counted only
where metrics are configured. Dropped the hard "eight", since two of
the eight are conditional.
The open question: what
/s/*actually servesSettled empirically, not by reading. Added
TestStaticServesEveryMethod(internal/server/routes_test.go),which drives the real router from
routes.goand requests/s/js/app.jswith GET, HEAD, POST, PUT and DELETE.Result: all five return 200. GET, POST, PUT and DELETE return the
asset bytes; HEAD returns 200 with an empty body. The reviewer's
suspicion was right — chi's
Mountregisters the handler for everymethod, and
http.FileServerspecial-cases only HEAD, by omitting thebody. The README's "
GETandHEAD" was aspirational.The route table row now reads
anyand states this, with the testnamed so the claim cannot drift silently again.
Re-sweep forced by the rebase
nextgained #145 (Alpinefetched and hash-verified at build time) while this branch sat. That
changed what is true in four sections this PR touches, so they were
re-derived against the rebased tree:
static/js/alpine.min.jsis no longer committed — theentry now says it is fetched by
script/fetch-assets— andstatic/vendor.sha256was absent; added.script/fetch-assetsbeforemake test; added to the stage description.numbers).
make fmt-checkwas missing from the Development Commandslist; added.
build. It said
make depsthenmake check.make depsonly runsgo mod download/tidy, so in a fresh clonemake checkfailsTestVendoredAssetsMatchManifestandTestBaseTemplateScriptsAreServedon the absent asset. It now saysmake bootstrap, which ends by runningscript/fetch-assets.Prerequisites gained
curl, which that script needs.script/entrypoint list verified complete in both directions againstls script/: 13 scripts, 13 documented, none stale.Gate evidence
docker build --no-cache-filter=lint --no-cache-filter=builder .onthe pushed tree (
92f3a01), exit 0:#20 [lint 7/8] RUN make fmt-checkDONE 1.0s#21 [lint 8/8] RUN make lint->0 issues.DONE 53.2s#28 [builder 8/11] RUN script/fetch-assetsDONE 2.1s#29 [builder 9/11] RUN make testDONE 60.1s — 11oklines withreal durations (
internal/delivery 4.983s,internal/handlers 3.573s,internal/database 2.289s,internal/server 1.998s,static 1.021s, ...), zero(cached)markers, zeroFAIL.TestStaticServesEveryMethodand its five subtests pass inside thecontainer.
#30 [builder 10/11] RUN make buildDONE 53.9smake checkon the host, exit 0,0 issues., run with an isolatedGOLANGCI_LINT_CACHEso no shared-cache bleed. No host lint findingcited a path outside the clone. The shared cache was not cleared.
Image
docker rmi'd;docker ps -aempty. Nodocker runwas issuedat any point and no prune of any kind.
Branch rebased onto
origin/next(c378690) and both gates re-run afterthe rebase, since the rebase changed the tree materially.
Reported rather than fixed, out of scope:
golangci-lintv2.12.2 warnsthat
gomodguardis deprecated in favour ofgomodguard_v2. Lint isgreen.
FAIL — needs-rework
The eight numbered items of #151 are all correctly fixed, and the sweep is genuinely broad — but it is not complete, and the claim table asserts as verified at least one statement that is false in the code. That is the failure mode this unit exists to close.
Sweep coverage I performed
Independently re-derived, not spot-checked: every route in the README tables method-by-method against
internal/server/routes.go(the six additions and the/metricsconditional registration are correct, and I found no route still missing); all 12 environment variables againstinternal/config/config.go(the table's 12 rows are exactly the 12os.Getenvreads;MAINTENANCE_MODEand theDATA_DIRQuick Start correction both confirmed); the whole Docker/CI section againstDockerfile,.dockerignoreand.gitea/workflows/check.yml; the fx wiring list andfx.Invokesignature againstcmd/webhooker/main.go; the package tree againstgit ls-files; and ~112 further claims across the data model, database architecture, request flow, worker pool, circuit breaker and rate limiting sections. Roughly 150 statements checked. Fifteen are wrong.Findings
1.
README.md:892-894— false, and asserted verified in the claim table.> "Circuit breakers only apply to HTTP targets with
max_retries> 0. Fire-and-forget HTTP targets (max_retries== 0), Slack targets, database targets ... do not use circuit breakers."internal/delivery/target_slack.go:36embeds*httpCore, and:84-87passesd.Target.MaxRetriesinto the shareddeliver.internal/delivery/target_http.go:61-69routes anymaxRetries != 0towithRetry, which takes a per-target breaker at:105. A Slack target withmax_retries> 0 gets a breaker with the same 5-failure / 30s defaults. The PR body lists this exact statement as "correct, left alone". It also contradictsREADME.md:838("Onlyhttpandslacktargets own durable retries") and the Target table at:519, which scopesmax_retriesto HTTP. Acceptable: state that the breaker covershttpandslacktargets withmax_retries> 0, fix the section heading, and fix themax_retries/max_queue_sizerows so they are not HTTP-only.2.
README.md:1271-1273— false, and introduced by this PR.> "Local linting goes through the same container rather than a host golangci-lint, because a host binary can be a different version from the pinned one."
script/lintrunsgolangci-lint run --config .golangci.yml ./...directly on the host;make lintis a shim over it. Onlyscript/cibuildandscript/dockerinvolve Docker. This is a new false statement in a PR whose deliverable is that the README contains none.3.
README.md:669and:680— false.> "won't cause lock contention or WAL bloat" / "each webhook's database has its own WAL, its own page cache, and its own lock"
No WAL is enabled anywhere. Both DSNs are
file:%s?cache=shared&mode=rwc(internal/database/webhook_db_manager.go:237,internal/database/database.go:139) with nojournal_modepragma, and noPRAGMAis issued in the tree. SQLite defaults to the rollback journal. Per-file page cache and per-file lock are real; the WAL is not.4.
README.md:738— false config key.> "Config stores
webhook_url— the Slack/Mattermost incoming webhook endpoint."SlackTargetConfig.WebhookURLis taggedjson:"webhookUrl"(internal/delivery/target_slack.go:27) and the create handler writeswebhookUrl(internal/handlers/source_management.go:1098). An operator following the README would produce config that is silently ignored and fails withwebhook_url is required.5.
README.md:624and:1244— false.> "All entities include these fields from
BaseModel" / "GORM soft deletes on all entities (data preserved for audit)"Setting(internal/database/model_setting.go:5-8) has onlyKeyandValue— noBaseModel, noid/created_at/updated_at/deleted_at, no soft delete. The README's own Setting table at:411-414already says so, so the document contradicts itself. The claim table lists the data-model tables as checked "field-by-field".6.
README.md:761and:797— nonexistent identifier.> "Build self-contained DeliveryTask structs" / "each
DeliveryTaskis sent to the delivery channel"The type is
delivery.Task(internal/delivery/engine.go:70);DeliveryTaskexists nowhere in the tree. This PR corrected exactly this identifier in the Dependency Injection section and left the two occurrences above it.7.
README.md:29— false, inside a hunk this PR edited.>
# Run all checks (format, lint, test, build)abovemake checkscript/checkruns test, lint, fmt-check — it does not build.README.md:51states the correct set two lines below, so the code block contradicts itself. The claim table lists "make check= test + lint + fmt-check | correct, left alone", which verified the second line and not the first.8.
README.md:495— false.> "
path| string | Unique URL path (UUID-based, e.g./webhook/{uuid})"The column stores a bare UUID (
internal/handlers/source_management.go:334and:934:Path: uuid.New().String()) and is matched against the raw chi param (internal/handlers/webhook.go:124-126). No/webhook/prefix is stored; that is route prefix only.9.
README.md:520— describes a field that does nothing.> "
max_queue_size| integer | Maximum queued deliveries (for HTTP targets with retries)"The field exists (
internal/database/model_target.go:29) but its only non-test readers are the display projection atinternal/delivery/target_config_view.go:152-155. Nothing in the engine or any target consults it. Queue depth is the two fixed 10,000-entry channels. Say plainly that it is stored and displayed but not yet enforced, in the same register the README uses for planned replay.10.
README.md:762and:792— off by one.> "event data inline for ≤16KB"
The test is
len(body) < delivery.MaxInlineBodySize(internal/handlers/webhook.go:244,internal/delivery/engine.go:1054), so a body of exactly 16384 bytes is not inlined. The constant's own comment says "at or above". Should read< 16 KiB.11.
README.md:467— the new three-band text mislabels the first band.> "
0up todatabase.MaxFiniteRetentionDays(106751 days ...) is accepted as a finite retention."Webhook.BeforeSave(internal/database/model_webhook.go:86-87) rewrites any value<= 0toRetentionForeverDays, so0belongs to the third band, not the first — as the paragraph directly above it already says. The finite range is 1 to 106751. The 400 band and the>=365000band are both correct againstparseRetentionDays.12.
README.md:909— contradicts:917-923.> "keyed per client IP per entrypoint"
internal/middleware/ratelimit.go:303-306useshttprate.KeyByEndpoint, i.e. per client IP per request path. The later paragraph states the path behaviour correctly and is the reason the aggregate limiter exists; the earlier sentence is the one that is wrong.13.
README.md:820-821— false.> "
WaitGroup.Wait()ensures clean shutdown."internal/delivery/engine.go:302callslifecycle.WaitForShutdown, which is bounded by fx's stop context and, on timeout, logs at ERROR, returns an error, and leaves goroutines running (internal/lifecycle/lifecycle.go:12-26) — the barewg.Wait()is documented there as precisely what must not happen.14.
README.md:1116— introduced by this PR, imprecise.> "
Makefile— Thin shims overscript/"build,run,dev,deps,cleanandcssare inline commands, not shims. Six of thirteen targets. The same overstatement pre-exists at:261-262; fix both or neither.15.
README.md:372— "webhooker's data model has eight entities". Nine are documented and ninemodel_*.gofiles exist (Setting, User, Webhook, Entrypoint, Target, APIKey, Event, Delivery, DeliveryResult).Minor, not blocking on their own: the package tree still omits the three non-test
testing.gofiles (internal/database,internal/middleware,internal/session); andREADME.md:1180"the eight global entries listed above all run first, so such a request is still logged, counted in the metrics" holds only when bothMETRICS_USERNAMEandSENTRY_DSNare set — six run by default and nothing is counted.What passes
Item 1 is correct and correctly scoped: the warning now fires on empty
TRUSTED_PROXIESin any environment, keying (rateLimitKey/clientKey), the limits, and the emptyTRUSTED_PROXIESdefault are all untouched, so nothing belonging to #150 was taken. The wording is accurate in both directions and does not cry wolf at a local developer.TestSharedRateLimitBucketWarningruns all four cases (both prod, both dev) and would catch a regression to theIsProd()gate. Items 2 through 8 are all correct againstDockerfile,internal/handlers/webhook.go,internal/config/config.goandTODO.md; theTODO.mdedits are confined to items 7 and 8. Both folded-in items are correct: the 413 text now matchesMaxBodySize(WARN with method, path and declared length, after the global stack), and the retention bands matchparseRetentionDaysapart from finding 11. Single commit, basenext, title ends(closes #151), noclosesfor #93 or #99, no Claude/Anthropic reference or attribution trailer anywhere in the diff, commit message or PR body. No conflicts against currentnext(9ae1915).Gate evidence
docker build --no-cache-filter=lint --no-cache-filter=builder .in a fresh clone at9ac8ff2, exit 0:#21 [lint 8/8] RUN make lint->0 issues.DONE 64.6s#20 [lint 7/8] RUN make fmt-checkDONE 1.6s#33 [builder 8/10] RUN make testDONE 63.4s — 10oklines with real durations (internal/delivery 4.811s,internal/handlers 3.632s,internal/database 2.084s,internal/server 1.960s, ...), zero(cached)markers, zero FAIL#34 [builder 9/10] RUN make buildexecutedThe image was removed and
docker ps -ais empty. Gitea CI on9ac8ff2issuccessin 3m10s.Disclosure: I did not run a host
make check— all linting was done inside the container per #106 / #109, and the Docker gate above covers all four targets. One claim I could not settle:README.md:981says/s/*servesGETandHEAD; chiMountregisters the handler for all methods andhttp.FileServerwill serve a POST body, so the claim may be aspirational rather than true. Not counted as a finding.9ac8ff28ecto92f3a016e1PASS
Independently re-derived from the code, not spot-checked: all 15 claim-table rows (including every row previously marked "correct, left alone"), both folded-in minors, all 8 numbered items of #151, the 4 re-swept rebase sections, and roughly 60 further statements — every documented route method-by-method against
internal/server/routes.go, all 12 environment variables againstinternal/config/config.go, all 9 entity tables column-by-column againstinternal/database/model_*.go, the package tree againstgit ls-files, the Docker/CI section againstDockerfile/.dockerignore/.gitea/workflows/check.yml, the fx wiring againstcmd/webhooker/main.go, and every intra-document anchor. No "correct" row was false this time. Retention boundaries checked at 0, negative, 106751, 106752, 364999, 365000. Circuit-breaker scope fixed in all six places; no seventh survives./s/*test drives the real router viaserver.NewRouterForTest->SetupRoutes, and all five methods genuinely return 200. Quick Start verified by running it:make bootstrapthenmake checkin a fresh clone, exit 0.Probes that passed for the right reason, worth recording: the 429
Retry-Afterclaim survives the custom limit handler only because httprate sets the header before invoking it (httprate@v0.15.0/limiter.go:100);GET /pages/loginreally is unlimited despite the middleware sitting on a group containing both verbs, becausepostRateLimitshort-circuits non-POST.Three residual nits, none blocking:
README.md:110— theRECEIVER_RATE_LIMITtable row still reads "per IP per entrypoint", the phrasing finding 12 corrected at:966("per client IP per request path"). Not failed on: the code names that limiterperEntrypoint(internal/middleware/ratelimit.go:300) and the section uses the same label throughout, so it reads as the limiter's name rather than a keying claim — but it is the one place the corrected phrase survives.templates/templates.gois the only non-test.gofile absent, becausetemplates/is collapsed to a directory line whilestatic/is expanded down tostatic.go. The PR body's "verified complete in both directions" is therefore slightly overstated.Out of scope, noted for follow-up:
internal/database/base_model.go:10still says "common fields for all models", the same statement the README fix corrected; and the Security bullet's "SSRF prevention for HTTP delivery targets" under-claims —slacktargets share bothValidateTargetURL(internal/handlers/source_management.go:1130) and the SSRF-safe transport (internal/delivery/target.go:88-91).Scope guards: item 1 touches only when the warning fires and its wording; keying, limits and the
TRUSTED_PROXIESdefault are untouched, so nothing belonging to #150 was taken.TODO.mdedits confined to items 7 and 8. Single commit, basenext, title ends(closes #151), noclosesfor #93 or #99. Fast-forwards onto currentnext(c378690). No Claude/Anthropic reference or attribution trailer anywhere in the diff, commit message or PR body.Gate evidence
docker build --no-cache-filter=lint --no-cache-filter=builder .in a fresh clone at92f3a01, exit 0:#15 [lint 7/8] RUN make fmt-checkDONE 0.2s#16 [lint 8/8] RUN make lint->0 issues.DONE 50.1s#23 [builder 8/11] RUN script/fetch-assetsDONE 0.7s#24 [builder 9/11] RUN make testDONE 54.4s — 11oklines with real durations (internal/delivery 4.077s,internal/handlers 3.682s,internal/database 2.109s,internal/server 1.988s,static 1.020s, ...), zero(cached)markers, zero FAIL.TestStaticServesEveryMethodand all five subtests pass in-container.#25 [builder 10/11] RUN make buildDONE 49.3sOnly the
stage-2Alpine layers were CACHED. Imagedocker rmi'd,docker ps -aclean, no prune of any kind, nodocker run.Host
make bootstrapthenmake check: exit 0, 11oklines, zero(cached),0 issues., run with an isolatedGOLANGCI_LINT_CACHE; no finding cited a path outside the clone, and the shared cache was not cleared. The container remains the authoritative lint result. Gitea CI on92f3a01issuccessin 2m59s.Disclosure:
make bootstrapskips installing the pinned linter if anygolangci-lintis already on PATH, so the Prerequisites line "make bootstrapinstalls it" holds for a fresh machine only — the README already says the host version can differ and the container is authoritative, so I am waiving it rather than filing it. Separately,golangci-lintv2.12.2 warns thatgomodguardis deprecated in favour ofgomodguard_v2; already reported by the author, out of scope here.Not a defect in this PR, raised for a separate decision:
/s/*answering POST/PUT/DELETE with 200 and the asset bytes is documented correctly here, but the underlying behaviour may itself be worth restricting to GET/HEAD.