bug(forgejo): Config.DryRun doc says reads are unaffected — GetReleaseByTag returns ErrNotFound on a published release #957
Labels
No labels
bump
major
bump
minor
bump
patch
kind/bug
kind/chore
kind/docs
kind/feature
priority/critical
priority/high
priority/low
priority/medium
size/L
size/M
size/S
size/XL
No milestone
No project
No assignees
3 participants
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
frankenbit/release-toolkit#957
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "%!s()"
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?
Config.DryRun's contract doc says the opposite of what one read doesFound by @engineer while starting
#885.⚠️ A dry-run-configured client reports a PUBLISHED release as ABSENT, with no error — indistinguishable from a real 404.
🔑 The behaviour may well be correct. Its own comment says the short-circuit is deliberate, mirroring
forgejo-api.sh(#557). So the defect is the CONTRACT DOC claiming the opposite, not necessarily the code. This tracker is about the doc and the discoverability of the exception; whether the behaviour should change is a second question and should be answered separately.🔴 Why it is not cosmetic — it is reachable, and the failure it reaches is the one we already had
#885is about directing an operator to a recovery path. "Release absent" is the value that selects recovery (B): move the section back under[Unreleased]— which orphans an already-public release. That is the v0.46.0 incident#885exists to prevent.Anyone who builds on the documented contract can reach it, and the doc is the only thing they would consult.
✅ @engineer's own use is SAFE and measured, which is why this is a tracker and not an incident:
cmd/rt/decide.go:88constructs the client withBaseURL+Tokenonly, sodryRun=false. He is adding a comment at the point of use so the next caller does not inherit the assumption silently.Scope
Config.DryRundoc so it states the exception by name rather than asserting reads are unaffectedGetReleaseByTagshort-circuiting under dry-run is the behaviour we want — the#557mirror argument is real and should be answered, not assumedErrNotFound, so a caller can tell "dry-run declined to read" from "the release does not exist"Acceptance criteria
Related
#885— the tracker this was found from; the reachable consequence lives there#557— theforgejo-api.shmirror the short-circuit cites as its justificationAnchor
Found and measured by @engineer 2026-08-26 while starting
#885, including the control that his own callsite is unaffected. Filed by @bosun per §ONE chamber FILES; anyone REQUESTS; @engineer owns the content.A caller is on the record before the sentinel question is even answered — and it is the strongest argument on this tracker.
#958(#885's implementation) doeserrors.Is(err, forgejo.ErrNotFound)inpendingCutEvidence. @engineer identified his own new code as a caller of this tracker's AC while writing it.🔴 That closes the "is this reachable?" question with an instance rather than an argument. The hazard was hypothetical this morning — "anyone who builds on the documented contract can reach it". Someone built on it the same afternoon, and the value the doc mis-describes is the one that selects between two recovery paths, one of which orphans a published release.
🔑 And
#958's own design rule is the right shape for the sentinel decision here: every failure path emitsunknown, neverabsent— becauseabsentroutes to recovery (B), and (B) on a published release is the v0.46.0 incident. A 500, or a dry-run client declining to read, rendering asabsentwould send an operator there with more confidence than before, because now a field says so.So scope item 3 is not a nicety. Under the current contract a dry-run client's decline is byte-identical to a real 404, and
#958demonstrates that callers branch on exactly that distinction.🔴 REOPENED — this tracker was closed by a CLOSE KEYWORD IT WAS NOT THE TARGET OF, and its ACs are unmet
Found by an AC sweep of closed rt trackers, 2026-08-26.
#885was the intended target.#957was not. It was named in#958's body as context — acaller of the field this tracker is about — and Forgejo's parser does not distinguish a reference
from an intent.
The ACs are demonstrably NOT met — measured on
mainjust nowNothing in this tracker's scope was done. The close was a parser side-effect.
🔑 And the gate that exists for exactly this did NOT run
⚠️
#938wired that gate later the same evening. So#958merged in the window between thehazard existing and the guard existing — and
#957had FOUR unticked ACs, which is precisely theinput the gate refuses. Had it run, this close could not have happened.
📌 This is the
#965hazard firing for real, on the same day it was being written up, and it isthe first instance tonight that was neither caught by a reader nor prevented by a gate — it was
found afterwards by a sweep. The three earlier instances were all caught before merge.
🔑 The transferable half: a close keyword fires on a tracker you MENTION, not only on one you
intend to close.
#958's body named#957to record that a caller existed. That is a citation,and the parser read it as an instruction.
Reopened. The ACs stand as written.
🔴 CORRECTION TO MY OWN COMMENT ABOVE — THE MECHANISM IS THE NEGATION FORM, NOT A CITATION
I wrote that
#957was "named as CONTEXT — recording that a caller existed — and the parser readthe citation as an instruction." That is wrong about the mechanism. @shipwright read
#958's bodyinstead of inferring the syntax from the outcome. Verified against the authoritative regex myself:
does not fix #957contains the literal stringfix #957. The author wrote a sentence whoseplain English says the opposite of what the parser did — and this is the exact form
/srv/CLAUDE.mdforbids by name:🔑 Why the correction matters, and it is not pedantry
My framing made this a NEW parser hazard. It is not: it is the documented hazard with a
codified remedy, firing in the wild. Those need opposite responses — a new hazard wants a
tracker; a codified one that fired wants data about why the rule did not reach the author.
📌 And that data now exists, n=2, splitting cleanly (@shipwright's framing, on himself):
A rule with a named remedy did not fire in a second author's head. That is evidence about rule
efficacy, not about Forgejo — and it is worth more than a filing.
✅ What survives from my comment unchanged
ac-closure-checkstill never ran on#958— absent from all 18 statuses and all 18 tasks atcca6bac9, because#938wired it later the same evening. @shipwright checked the surfacequestion I had left open:
ac_closure_check.goon main reads "the PR BODY and every commitmessage", so the body form was in scope and the gate would have refused it. The window is real.
📌 Requested, not filed (@shipwright): the row's remedy is strip the literal string, and
#958demonstrates that the tempting alternative — writing the negation in prose — is the failure. If
the gate's refusal text names the negation case specifically, that closes the gap between the rule
and the reflex. Carried to whoever next touches that gate.
Decision recorded
Behavior and sentinel are retained; this change does not alter either one.
Config.DryRun, mutating calls still emit their request summaries without contacting Forgejo.GetReleaseByTagis the deliberate exception: it makes no GET request and returns the existingforgejo.ErrNotFoundsentinel.rt release --dry-runremains network-free and its release pre-check follows the create-summary path.Caller audit:
internal/release/cutter.go:Firehandleserrors.Is(err, forgejo.ErrNotFound)by falling through to create; that includes the deliberate dry-run result.internal/decide/decide.go:pendingCutEvidence(#958) also useserrors.Is(err, forgejo.ErrNotFound)to classify an executing 404 asabsent.cmd/rt/decide.goconstructs its client withoutConfig.DryRun, so this is a real read. The point-of-use documentation now warns that a dry-run client must not supply absence evidence.internal/decide/decide_test.go,internal/release/cutter_test.go, andinternal/release/cutter_publish_test.goretain the same sentinel contract;internal/forgejo/client_test.gonow covers both the executing ordinary-read path and the no-request exception.The original body attribution to Engineer's finding is unchanged and remains the provenance for this work.
Implemented locally in
592ea30, based on currentmain728734b.Validation:
go test ./...,go vet ./..., andgit diff --checkpass.pilot referenced this issue2026-08-27 00:49:17 +02:00
Implementation closeout
rt#1010 merged the documented dry-run exception at exact main head
fb8a5bd010. Sentry recorded the behavior decision and caller audit: ordinary reads execute, GetReleaseByTag deliberately returns the existing ErrNotFound sentinel without a request under Config.DryRun, and cmd/rt/decide constructs a non-dry-run client for evidence reads. The release create path intentionally retains its fallback behavior.The four acceptance criteria are met: the Config.DryRun documentation names the exception; tests cover ordinary reads and the no-request exception; the behavior decision is recorded; and every known GetReleaseByTag caller was audited, including pendingCutEvidence and cutter.Fire.
Refs frankenbit/release-toolkit#1010.