forge label create exits 22 without diagnostics #100
Labels
No labels
bug
duplicate
enhancement
help wanted
invalid
question
wontfix
bug
duplicate
enhancement
help wanted
invalid
question
wontfix
No milestone
No project
No assignees
3 participants
Notifications
Due date
No due date set.
Dependencies
No dependencies set
Reference
allod/tools#100
Loading…
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?
User story: So that I can fix label setup failures without guessing,
forge label createshould report useful API or validation diagnostics whenever label creation is rejected.Part of the "Forge / workspace CLI tooling" arc.
Bug
forge label createcan fail with exit code 22 and print no error message, leaving the operator with no actionable cause.Observed while trying to create a priority label on a repository with no existing labels:
Result:
22A prior variant without the leading
#in the color also failed silently:Authentication was valid at the time, and
forge label liststill reported no labels afterward.Expected behavior
If the Forgejo API rejects label creation,
forgeshould print the HTTP status and response body, or a concise decoded error message. If the failure is client-side argument validation, it should report the invalid field.At minimum, a failing
forge label createshould not exit nonzero with no diagnostics.Acceptance criteria
forge label create ...failures include enough context to distinguish auth/permission, validation, route, and server errors.Reproduced on the current
forge, with one new observation: the failure is not universal. The same command, same token, same session, succeeds on some repositories and fails on others.Exit 22, empty stdout, empty stderr, no label created:
Exit 0, prints
Label created: <id> bug, on repositories owned by the calling account rather than by the org.forge label delete <name> --yesalso succeeds there, so the whole create/delete round trip works on those repositories.The color argument is not the variable. The org-repo call fails with
-c '#d73a4a', with a bare name and no flags at all, and with or without-d.forge auth statusreports the credential as fine, andforge label listreturns exit 0 on every repository tried, org or not, printingNo labels in <repo>— no repository in this forge has a label defined.On why nothing is printed:
api()runscurl -sf, so 22 is curl's generic "HTTP >= 400" exit and both the status line and the response body are discarded before any handler could read them. That is upstream of everyforgesubcommand, not specific tolabel create.One adjacent observation from the same session: pushing an agent branch to one of the org repositories above is refused server-side with
Forgejo: User permission denied for writing.Correcting the theory in the comment above. I said creation succeeds "on repositories owned by the calling account rather than by the org". That is not the boundary. It also succeeds on a repository in a personal namespace that the calling account does not own — same token, same session, exit 0, label created and deletable.
So the split is not repository ownership. It is which namespaces the token carries write scope for: none for the org namespace, write for the personal namespaces it is provisioned against. Which means the underlying
403is correct behavior, not a bug. Label creation on an org repository is a human act at the forge, in the same category as branch protection, and no amount of fixingforgewill change that.That narrows this issue to what it actually is: a diagnostics defect, not an authorization one. The API call is being answered correctly and the answer is being thrown away.
api()runscurl -sf, so-fsuppresses the body and collapses every HTTP >= 400 into curl's generic exit 22 before any handler can read the status line. A correct, informative403 Forbiddenreaches the process and is discarded.The fix is therefore upstream of every subcommand rather than in
label create: stop passing-f, capture the status (-w '%{http_code}'or--fail-with-body), and report status plus response message on failure. Had the status been visible, the answer here would have been one line instead of a repository-by-repository bisect — which is the general cost of this bug, and the reason it is worth fixing at theapi()layer rather than per command.Fix prepared, awaiting relay — the calling account cannot push to this repository, so it is a commit on
agent/forge-api-error-diagnosticsrather than a PR.Before and after, on the exact command from the report:
The change is at the
api()layer, so every subcommand gains it rather thanlabel createalone.-fis dropped, the status is appended to the body with-w '\n%{http_code}'and split back off, and one line goes to stderr naming the method, path, status, and the API's ownmessage. Exit 22 is preserved for HTTP errors so existing callers see no contract change — including the one caller that deliberately tolerates failure — while a genuine transport failure now returns curl's real code instead of being flattened.Two things worth knowing for whoever reviews it. Some endpoints reject with an empty body:
POST /repos/{owner}/{repo}/issues/{n}/labelsreturns a bare 403, so there the output is justHTTP 403with no trailing message. And the mock curl in the test harness needed a central status append, because real curl honours-won every request while the mock only did so for the verification endpoint — a per-route append silently starved every caller that splits the status back off, which is worth remembering before adding routes.Covered by a new
tests/forge/api-errors.sh, verified to fail against the oldapi(): withcurl -sfrestored, a 403 renders asIssue #403: nullwith a jq type error, because the error body was being consumed as data. All nine forge suites pass with the change.This also settles the authorization question the earlier comments circled: the 403 is correct behavior, not a defect. The token carries issue-write scope; the account simply has
push: falseon org repositories, and label writes need repository write. Nothing inforgecan or should change that.Read-only review pass done on the prepared branch; findings and resolutions below. Follow-up commit
a0f9c97sits on top of983b96fonagent/forge-api-error-diagnostics. No blockers were found in the original commit.The review verified byte-identical stdout, stderr, and exit codes across 148 command invocations of the eight pre-existing suites, probed the body/status split against ten body shapes, and mutation-tested the new fixture. Worth recording what that established: the split is correct on empty, multi-line, trailing-newline-or-not, and numeric-final-line bodies; the token still never reaches argv; and exit 22 survives every caller style including the one that deliberately tolerates failure.
Acted on:
run_failasserts only a non-zero exit, so degradingreturn 22toreturn 1left all nine suites green — the load-bearing half of what the docs promise was unvalidated. Added direct exit-code assertions plus mock fixtures for a transport failure and a redirect, then confirmed by mutation that each of the three now fails when broken.http://base URL against an https-only forge draws a 308 with an empty body, and the old test reported that asNo labels in <repo>with exit 0 — a confidently wrong answer, which is worse than the silence this issue is about. Now any non-2xx reports. Redirects are included deliberately: no-Lis passed, so a 3xx body is never the requested resource.verify_token_httphad the same defect this change removes. Itscurllacked thercguard, so underset -euo pipefailan unreachable host produced completely empty output —auth statusagainst a nonexistent host printed nothing at all and exited 6. It now reportscurl exit 6.messagecontaining a newline produced two stderr lines, control bytes reached the terminal raw, the.messagepath was untruncated while the raw-body fallback was capped, and a JSONnullbody rendered asHTTP 404: nullas though the API had said "null". The text is now flattened to one line, stripped of control bytes, truncated uniformly, and a null is treated as absent.issues/403*also captured four-digit issue numbers, and a comment claimed routes set a non-200 status when the documented 204 routes do not.Not acted on, recorded instead:
api GET /userwould bypass it silently, and the mock models auth failure as a transport failure, which leaves the HTTP 401 path untestable.pmscripts run the samecurl -sfpattern with the token in argv. Outside this issue's scope and filed separately as issue #133, along with a second defect in the same lines:2>/dev/null || truemeans a failed page silently truncates the data, which turns the integrity check into a no-op that still exits 0.shellcheckis not installed on the machine this was prepared on, so no lint pass was run.bash -nis clean on all three shell files and all nine suites pass.is this fixed now? can we close this issue?
The Go
forgethat is now authoritative satisfies this issue's acceptance criteria. Non-2xx responses report the method, path, HTTP status, and a sanitized API message or response-body fallback while preserving exit 22; transport failures keep their own mapped exit code. Color handling accepts and normalizes both#FF0000andFF0000, and rejects malformed values before the request.The retained tests pin exact API-error output, redirects, empty and malformed response bodies, transport failures, both accepted color forms, and invalid color validation (
cmd/forge/harness_test.goandcmd/forge/api_errors_test.go). PR #157 removed the old Bash implementation that still contained the silent-failure path, after the Go package had passed live deployment witnesses on bothallod-devandnexus.