fix: harden get_issue parsing and surface real errors (#27); align CI image publish

get_issue raised 'NoneType' object is not iterable on issues whose
labels/assignees Gitea returns as null or with non-dict elements (the #13
class), which reached clients as an opaque JSON-RPC -32603 with no detail.

- read_tools: skip non-dict label/assignee entries in get_issue_tool
- server: detect a wrapped GiteaNotFoundError via the __cause__ chain and
  return 404 / JSON-RPC -32000 with a clear message; include the exception
  type name in masked internal errors so future masked failures are
  diagnosable without exposing messages or stack traces
- tests: cover non-dict collection elements and the not-found / typed-error
  responses
- ci: rewrite docker.yml to build, smoke-test and push the image to the
  Gitea container registry on merge to main/dev, matching the hiddenden.cafe
  pattern (only REGISTRY_TOKEN required)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-25 16:51:58 +02:00
parent 026f3a654f
commit 41749fd7b4
5 changed files with 295 additions and 156 deletions
+27
View File
@@ -303,6 +303,33 @@ async def test_get_issue_tolerates_null_collections() -> None:
assert result["assignees"] == []
@pytest.mark.asyncio
async def test_get_issue_skips_non_dict_collection_elements() -> None:
"""Defense-in-depth for #27: tolerate non-dict entries inside labels/assignees.
A stray null/non-object element would otherwise raise AttributeError when
`.get()` is called on it, surfacing as an opaque internal error.
"""
class MalformedElementsGitea(StubGitea):
async def get_issue(self, owner, repo, index):
return {
"number": index,
"title": "Issue",
"body": "Body",
"state": "open",
"user": {"login": "alice"},
"labels": [{"name": "bug"}, None, "weird"],
"assignees": [{"login": "bob"}, None, 42],
}
result = await get_issue_tool(
MalformedElementsGitea(), {"owner": "acme", "repo": "app", "issue_number": 1}
)
assert result["labels"] == ["bug"]
assert result["assignees"] == ["bob"]
@pytest.mark.asyncio
@pytest.mark.parametrize(
"tool,args,expected_key",