Silent label no-op in edit_issue and create_issue -- the forgejo.ts fix was never ported to the Python client #18
Labels
No labels
api
architecture
auth
authentication
blocked
bug
chore
ci/cd
codec-constraints
component:blitz
component:chatter
component:entertainment
component:foodchain
component:forgejo-client
component:gatekeeper
component:mappy
component:monads
component:spiffy
deduplication
dependencies
documentation
enhancement
feature
fix
graphics
in-progress
lang:go
lang:python
lang:rust
lang:shell
lang:toml
lang:typescript
performance
pkg:agent
pkg:ai
pkg:coding-agent
pkg:tui
priority:critical
priority:high
priority:low
priority:medium
ready
resolution
review
security
technical-debt
testing
tracking
vendor
video-encoding
workgroup
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set
Reference
kade/forgejo-client#18
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?
Summary
~/src/forgejo-clientcarries the same silent label no-op that was found and fixed in theTypeScript
forgejo.tson 2026-10-01. The archived brief scoped itself topackages/coding-agent/src/core/tools/forgejo.ts; this Python client is a different repo andwas never in it. The companion zero-tests note for this repo has zero occurrences of the string
label— nobody has examined it.Both defects share a shape: accept a
labelsargument, do nothing with it, return success.1.
edit_issuepasses labels through rawsrc/forgejo_client/modules/issues.py:189This is a
PATCH /repos/{owner}/{repo}/issues/{n}. Two independent problems:labelsfield at all. Measured againstgit.sly.soon 2026-10-01:PATCHwith labels as names returns 200 and changes nothing;the same request with numeric label IDs returns 200 and changes nothing.
update_issue(:199-202) andclose_issue(:204) both delegate here, so the entire updatesurface inherits the no-op.
This is verbatim the defect the archived brief recorded as "a no-op that returns success" and
told the implementer to fix by either routing through
POST /issues/{n}/labelsor rejecting thefield outright. Both resolutions remain available here.
2.
create_issuesilently drops labels that do not resolvesrc/forgejo_client/modules/issues.py:88The name→ID conversion works, but there is no
else. A name that resolves to nothing isdropped with no error and no warning. Two further holes in the same block:
success that looks complete.
data['labels']is never set and the issue is created unlabelled.This was observed live, not inferred.
AGENTS.md§9 documents fourpkg:*labels(
pkg:agent,pkg:ai,pkg:coding-agent,pkg:tui).kade/rhad three of the four —pkg:aidid not exist. Passing--label pkg:aitherefore succeeded and applied nothing. Themissing label was invisible precisely because the failure was silent; it was found only by
listing the repo's labels after the fact.
pkg:aihas since been created (id 637).This repo currently has no
pkg:*labels at all, so any attempt to label an issue here withthe documented names will silently drop every one of them.
The comment directly above this block (
issues.py:85-87) reads "For now, we'll skip labels toavoid the API error / TODO: Implement label name to ID conversion" — stale, and contradicted by
the twelve lines of working conversion code beneath it.
3.
LabelsModuleis complete and unreachable from the CLIsrc/forgejo_client/modules/labels.pyimplementsget_labels,create_label,update_label,delete_labelandensure_labels.forgejo_client.cliexposes nolabelssubcommand, sonone of it is reachable from the CLI.
ensure_labelsis dead code.This settles an open question from the archived brief, which asked whether label creation
belongs in the tool and concluded "my recommendation is that it belongs". For the Python client
it already exists and is simply not wired up.
Working path today, using the library directly:
4. Duplicate label names in this repo
While enumerating labels:
bugexists as both id 384 and id 212, andci/cdas both 421 and219. Name→ID resolution takes the first match in list order, so which label an issue receives is
not determined by its name. Worth deduplicating separately.
The working procedure, for reference
GET /api/v1/repos/{owner}/{repo}/labels— enumerate to obtain numeric IDs.POST /api/v1/repos/{owner}/{repo}/issues/{n}/labelswith{"labels":[id,...]}— additive.This is the only call that attaches a label to an existing issue.
PUTon the same path — replace semantics.POST /api/v1/repos/{owner}/{repo}/labelswith{name,color,description}— creates a label.forgejo.ts:308-311states the constraint in the harness tool: "Label operations cannot routethrough forgejo-cli: its parser exposes only
issues create|update|list|comment,search,wiki syncandcache, so there is no argv that reaches/issues/{index}/labels."Definition of done
edit_issueeither resolves names→IDs and routes throughPOST /issues/{n}/labels, orrejects
labelswith an error naming that route. Accepting the field and doing nothing isnot an acceptable resolution.
create_issueraises on any unresolvable name, listing the unknown names and the ones thatdo exist.
forgejo.tsalready has this asthrowUnknownLabels()— port it. The partial casemust not be a silent success either.
labelsCLI subcommand exposing the existingLabelsModule.issues.py:85-87corrected.Tests
This client has no test suite. The archived zero-tests note covers a different defect — a
src/directory shadowing the installed package — so "no tests" is not solved here.Per the anti-vacuity rule that the TS brief was held to: a test asserting only that
POST /issues/{n}/labelswas called passes against the broken code, because the original bugwas a different endpoint being called successfully. The test that catches it asserts that an
unresolvable label name produces an error and sends no request at all. Same shape as
fails on an unresolvable label name and sends no mutation at allin the TS suite.Cross-references
~/.todos/archive/ACTIONABLE-2026-09-30-forge-forgejo-label-support.md(verified 2026-10-01;forgejo.tsis 1208 lines with 67 occurrences oflabels)~/.todos/archive/ACTIONABLE-2026-09-28-forgejo-client-zero-tests-ran-src-shadowing.md~/.todos/ACTIONABLE-2026-10-01-forgejo-client-label-no-op-and-the-missing-pkg-ai-label.mdkade/rshipped three of four documentedpkg:*labels becausethe fourth's absence was invisible.
Progress update — the two silent no-ops are fixed
~/src/forgejo-clientmain, uncommitted.cli.pyandtests/test_cli.pyin theworking tree are another agent's in-flight work and are not touched here.
Definition of done, item by item
edit_issuerejectslabels— done. RaisesNotImplementedErrornamingPOST {issues_url}/{n}/labels.update_issueandclose_issueinherit it; both arethin delegates and never had a working label path.
I took the reject branch rather than routing through
POST /issues/{n}/labelsbecause rejecting cannot rot: if Forgejo ever starts honouring the field, the error
becomes visibly wrong and gets fixed. Silently relabelling every caller would have
changed behaviour for anyone currently relying on the no-op.
create_issueraises on unknown names — done, with the partial case covered too.New
_resolve_label_idsraises on:the API returned first;
['bug', 'ghost']does not create the issue.Stale comment at
issues.py:85-87— done. Removed.One addition the original report missed
The resolver reads labels with
use_cache=False, and this is load-bearing ratherthan tidy.
cache_ttlis 3600s. A label created minutes ago would resolve asunknown, so the new check would fail spuriously and every caller would learn to
retry around it — converting a silent drop into a loud wrong answer.
ensure_labelshad the same latent problem: a stale read reports a label that existsas missing and then attempts to create it a second time. It now reads uncached too.
Tests — 7 regressions, and proof they are not vacuous
tests/test_issues.py::TestLabelResolution, following the anti-vacuity style alreadyestablished by
TestIssueReadCachingin the same file.The first attempt at proving they guard anything passed against the pre-fix code,
which is the failure mode this discipline exists to catch.
PYTHONPATHdid not win:conftest.py:20doessys.path.insert(0, .../src), putting the fixed source ahead ofit, and
--confcutdirdid not stop that. Building a self-contained copy whose ownconftest.pypointed at the pre-fixissues.pygave the honest answer:The seventh passes by design — it asserts
edit_issuestill edits title/body/state,i.e. that the fix did not break the fields that do work.
Full suite: 285 passed, 1 skipped, no regressions.
The repository's own label set was part of the bug
kade/forgejo-clienthad 55 labels and none of the fourpkg:*, so any--label pkg:*there dropped every one. It also had seven duplicate names, whichis precisely the condition the new ambiguity check exists to catch:
bug(384/212),enhancement(385/171),documentation(387/214),ci/cd(390/220),
dependencies(391/223),feature(386/213),tracking(394/215).Ids 212–223 were a bulk import with templated
<name> issuesdescriptions; 384–394is a curated set with real descriptions and GitHub-standard colours. Migrated to the
curated set, four
pkg:*created (638–641), each removal gated on the canonicalalready being attached to that issue. A
POST /issues/{n}/labelsis additive and doesnot remove the loser — the per-issue assertion caught that before any label object
was deleted.
Result: 55 → 52 labels, 17 → 17 issues, no issue lost a label, zero duplicates.
pyproject.tomlhas an empty[tool.ruff]section, yetissues.pyat HEAD had 63 violationsWorth knowing before assuming a lint failure is yours.
issues.pyandlabels.pyarenow clean under
EXE001,I001,FA100,E,W,F— the set the harness edit tool enforces —and under
--select ALLapart from two deliberate exceptions: the optional-sklearnimports, which must stay function-local or the package will not import without sklearn,
and the
FBT001/FBT002boolean-positional-argument warnings, which would requirebreaking the public signature of
get_issues,find_similar_issues,search_issuesand
search_with_filters. Every in-repo caller already passes those by keyword, so itis a one-line change if breaking external callers is acceptable.
Still open
labelsCLI subcommand wrapping the existing, completeLabelsModule, soensure_labelsis reachable and label creation stops being a manual step.delete_labelpasses a label name toDELETE .../labels/{name}. Not verifiedagainst Forgejo and deliberately not changed on a guess.
Full write-up:
~/.todos/ACTIONABLE-2026-10-01-forgejo-client-label-no-op-and-the-missing-pkg-ai-label.md