fix(tools): convert label names to int64 IDs for Forgejo create-issue #17

Merged
slaid098 merged 1 commit from fix/create-issue/labels-int64 into main 2026-08-07 13:19:48 +03:00
Owner

Что сделано

Реализован lookup label names → int64 IDs в .opencode/tools/_shared.ts для Forgejo create-issue API.

  • Добавлена функция resolveLabelIds(full, names, opts): GET /repos/{repo}/labels → строит map name → id, для каждого имени из --labels находит ID
  • Несуществующие labels автосоздаются через POST /repos/{repo}/labels с дефолтным цветом #ededed (agents могут использовать labels которых ещё нет на репо)
  • Ветка issue create в callForgejoGh теперь отправляет labels: [int64, ...] (НЕ строки) в POST /repos/{repo}/issues
  • Пустой массив labels → поле labels НЕ отправляется (раньше отправлялся пустой массив → 422 на некоторых версиях Forgejo)
  • Обработка ошибок: при провале lookup/auto-create → возвращается error string, issue НЕ создаётся (all-or-nothing)
  • Внешний контракт tool'а НЕ меняется: labels: string[] имён остаётся (обратная совместимость с GitHub-режимом и существующими вызовами)

Тесты в tests/test_create_issue_tool.py:

  • test_labels_passed_to_gh — обновлён: проверяет GET /labels + POST /issues с int64 IDs [1, 2] (вместо строк)
  • test_labels_missing_autocreate — новый: несуществующий label → POST /labels (auto-create) + POST /issues с новым ID [42]
  • test_labels_empty_omitted — новый: пустой labels → поле labels отсутствует в POST body
  • test_labels_lookup_failure — новый: GET /labels падает → error surfaced, issue не создан

Почему

Forgejo API POST /api/v1/repos/{owner}/{repo}/issues ожидает CreateIssueOption.labels как []int64 ID лейблов, НЕ строковых имён (в отличие от GitHub API который принимает строки). Tool отправлял ["enhancement"] → Forgejo возвращал HTTP 422 Unprocessable Entity. Контракты GitHub vs Forgejo расходятся — нужен внутренний lookup (issue #4).

Решение по несуществующим labels: auto-create (а не error) — agents в автоматическом режиме не могут вручную создавать labels через UI, error блокировал бы pipeline. Auto-create с дефолтным цветом решает это; если и auto-create падает → возвращается понятная ошибка с именем label.

create-pr.ts НЕ использует labels (проверено) → баг #4 scope только в create-issue, отдельный issue не нужен.

Watch out

  • Тест-харнес _ts_loader.mjs не парсит TS-генерики в return-type inline-объектах (Promise<{...}>) и as assertions — пришлось вынести LabelResult/Label в type-aliases и убрать as assertion (runtime JSON.parse возвращает any в JS). При добавлении новых typed helpers в _shared.ts — использовать type-aliases, не inline-генерики
  • Token в remote URL для push: Forgejo не принимает https push без auth. Workaround git remote set-url origin https://slaid098:${FORGEJO_TOKEN}@... для push, затем возврат к чистому URL. НЕ коммитить URL с token
  • Coverage FAIL при одиночном запуске pytest tests/test_create_issue_tool.py — pre-existing (TS не покрывается Python coverage). Полный pytest набор проходит (83.53%)
  • Pre-existing mypy error check-permissions.py:114 no-any-return — НЕ введён этим PR (на main, см. memory PR#14)
  • ruff check/ruff format — все green, mypy green

Pending

  • Smoke-test на живом Forgejo репо: создать тестовый issue с labels через tool → убедиться что label прикреплён (GET /issues/{n} вернёт labels массив не пустой). Не выполнен в PR (требует живого токена и репо, оставлен для verification после merge)
  • Issue #9 (create-pr tool не передаёт head field) — НЕ пофиксен этим PR, остаётся открытым. Этот PR создан через raw curl workaround (как PR#12, PR#13)

Closes #4

## Что сделано Реализован lookup label names → int64 IDs в `.opencode/tools/_shared.ts` для Forgejo create-issue API. - Добавлена функция `resolveLabelIds(full, names, opts)`: `GET /repos/{repo}/labels` → строит map `name → id`, для каждого имени из `--labels` находит ID - Несуществующие labels **автосоздаются** через `POST /repos/{repo}/labels` с дефолтным цветом `#ededed` (agents могут использовать labels которых ещё нет на репо) - Ветка `issue create` в `callForgejoGh` теперь отправляет `labels: [int64, ...]` (НЕ строки) в `POST /repos/{repo}/issues` - Пустой массив labels → поле `labels` НЕ отправляется (раньше отправлялся пустой массив → 422 на некоторых версиях Forgejo) - Обработка ошибок: при провале lookup/auto-create → возвращается `error` string, issue НЕ создаётся (all-or-nothing) - Внешний контракт tool'а НЕ меняется: `labels: string[]` имён остаётся (обратная совместимость с GitHub-режимом и существующими вызовами) Тесты в `tests/test_create_issue_tool.py`: - `test_labels_passed_to_gh` — обновлён: проверяет GET /labels + POST /issues с int64 IDs `[1, 2]` (вместо строк) - `test_labels_missing_autocreate` — новый: несуществующий label → POST /labels (auto-create) + POST /issues с новым ID `[42]` - `test_labels_empty_omitted` — новый: пустой labels → поле `labels` отсутствует в POST body - `test_labels_lookup_failure` — новый: GET /labels падает → error surfaced, issue не создан ## Почему Forgejo API `POST /api/v1/repos/{owner}/{repo}/issues` ожидает `CreateIssueOption.labels` как `[]int64` ID лейблов, НЕ строковых имён (в отличие от GitHub API который принимает строки). Tool отправлял `["enhancement"]` → Forgejo возвращал HTTP 422 Unprocessable Entity. Контракты GitHub vs Forgejo расходятся — нужен внутренний lookup (issue #4). Решение по несуществующим labels: **auto-create** (а не error) — agents в автоматическом режиме не могут вручную создавать labels через UI, error блокировал бы pipeline. Auto-create с дефолтным цветом решает это; если и auto-create падает → возвращается понятная ошибка с именем label. `create-pr.ts` НЕ использует labels (проверено) → баг #4 scope только в create-issue, отдельный issue не нужен. ## Watch out - Тест-харнес `_ts_loader.mjs` не парсит TS-генерики в return-type inline-объектах (`Promise<{...}>`) и `as` assertions — пришлось вынести `LabelResult`/`Label` в type-aliases и убрать `as` assertion (runtime `JSON.parse` возвращает `any` в JS). При добавлении новых typed helpers в `_shared.ts` — использовать type-aliases, не inline-генерики - Token в remote URL для push: Forgejo не принимает https push без auth. Workaround `git remote set-url origin https://slaid098:${FORGEJO_TOKEN}@...` для push, затем возврат к чистому URL. НЕ коммитить URL с token - Coverage FAIL при одиночном запуске `pytest tests/test_create_issue_tool.py` — pre-existing (TS не покрывается Python coverage). Полный `pytest` набор проходит (83.53%) - Pre-existing mypy error `check-permissions.py:114` no-any-return — НЕ введён этим PR (на main, см. memory PR#14) - `ruff check`/`ruff format` — все green, mypy green ## Pending - Smoke-test на живом Forgejo репо: создать тестовый issue с labels через tool → убедиться что label прикреплён (GET /issues/{n} вернёт labels массив не пустой). Не выполнен в PR (требует живого токена и репо, оставлен для verification после merge) - Issue #9 (create-pr tool не передаёт `head` field) — НЕ пофиксен этим PR, остаётся открытым. Этот PR создан через raw curl workaround (как PR#12, PR#13) Closes #4
fix(tools): convert label names to int64 IDs for Forgejo create-issue
All checks were successful
CI (always) / bootstrap (pull_request) Successful in 8s
CI / bootstrap (pull_request) Successful in 10s
CI / lint (pull_request) Successful in 34s
CI / complexity (pull_request) Successful in 34s
CI / typecheck (pull_request) Successful in 34s
CI / test (3.13) (pull_request) Successful in 1m42s
eea30afec8
Author
Owner

Code Review Summary

Реализация корректна: resolveLabelIds() делает GET /labels → name→id map, автосоздаёт недостающие labels (POST /labels, color #ededed), передаёт labels: [int64, ...] в POST /issues, пустой массив → поле опускается. Внешний контракт tool'а (labels: string[] имён) сохранён — обратная совместимость не нарушена. All-or-nothing error handling: при провале lookup/auto-create issue не создаётся. 4 теста покрывают все сценарии (int64 IDs, autocreate, empty omitted, lookup failure). Полный набор зелёный: 785 passed, ruff clean для изменённых файлов.

Positives

  • resolveLabelIds() — чистая, хорошо задокументированная функция (JSDoc объясняет «зачем»: Forgejo ожидает []int64, не строки)
  • All-or-nothing error handling реализован правильно: resolved.error → return {status:1} до POST /issues (строки 218-220)
  • Внешний контракт сохранён: create-issue.ts:24 остаётся labels: string[], tool сам конвертирует — существующие вызовы не ломаются
  • Пустой labels → поле опускается (...(labelIds.length > 0 ? {labels: labelIds} : {}), строка 226) — исправляет 422 на некоторых версиях Forgejo
  • Тесты качественные: проверяют и method (GET/POST), и URL path, и body content (int64 IDs, не строки), и количество fetch calls
  • test_labels_lookup_failure проверяет, что issue НЕ создаётся при провале lookup (assert 1 fetch call, не 2)

Suggestions (info, not blocking)

  • .opencode/skills/issue/SKILL.md:155-163 [docs] Документация устарела после этого PR. Строки 155-163 говорят: «Если label не существует в репо — tool упадёт. Создай через curl к Forgejo API...». Теперь tool автосоздаёт labels — эта инструкция вводит агентов в заблуждение: они будут делать лишний curl POST /labels перед вызовом tool (не ломает функциональность — label уже будет существовать в map — но лишняя работа). Рекомендуется обновить: заменить блок 155-163 на «Если label не существует — tool автосоздаст его с дефолтным цветом #ededed. Явное создание через curl нужно только если хочешь кастомный color».
    Fix: обновить issue/SKILL.md:155-163 в отдельном PR (cosmetic docs, не блокирует merge).

  • .opencode/tools/_shared.ts:119 [robustness] GET /repos/${full}/labels без ?page= параметра возвращает только первую страницу (Forgejo default page size = 50). Если в репо >50 labels, label на странице 2+ не найдется в map → автосоздастся как дубликат (POST /labels создаст label с тем же name — Forgejo не запрещает дубликаты имён). Для репозиториев slaid098 (несколько labels) это unlikely, но для будущих масштабов стоит учесть.
    Fix (если понадобится): добавить пагинацию — цикл ?page=1&limit=50 пока ответ не пустой, или ?limit=10000 (Forgejo поддерживает большой limit). Не блокирует merge — текущие репо <10 labels.

  • .opencode/tools/_shared.ts:127-146 [design] Auto-create без подтверждения: агент с опечаткой в label name (например "bugg" вместо "bug") молча создаст мусорный label. Для single-user tool это приемлемо (YAGNI), но стоит задокументировать в issue/SKILL.md как known behavior. Не требует флага подтверждения — добавит сложность ради редкого кейса.

Verdict: APPROVE

## Code Review Summary Реализация корректна: `resolveLabelIds()` делает GET /labels → name→id map, автосоздаёт недостающие labels (POST /labels, color `#ededed`), передаёт `labels: [int64, ...]` в POST /issues, пустой массив → поле опускается. Внешний контракт tool'а (`labels: string[]` имён) сохранён — обратная совместимость не нарушена. All-or-nothing error handling: при провале lookup/auto-create issue не создаётся. 4 теста покрывают все сценарии (int64 IDs, autocreate, empty omitted, lookup failure). Полный набор зелёный: 785 passed, ruff clean для изменённых файлов. ### Positives - `resolveLabelIds()` — чистая, хорошо задокументированная функция (JSDoc объясняет «зачем»: Forgejo ожидает `[]int64`, не строки) - All-or-nothing error handling реализован правильно: `resolved.error` → `return {status:1}` до POST /issues (строки 218-220) - Внешний контракт сохранён: `create-issue.ts:24` остаётся `labels: string[]`, tool сам конвертирует — существующие вызовы не ломаются - Пустой labels → поле опускается (`...(labelIds.length > 0 ? {labels: labelIds} : {})`, строка 226) — исправляет 422 на некоторых версиях Forgejo - Тесты качественные: проверяют и method (GET/POST), и URL path, и body content (int64 IDs, не строки), и количество fetch calls - `test_labels_lookup_failure` проверяет, что issue НЕ создаётся при провале lookup (assert 1 fetch call, не 2) ### Suggestions (info, not blocking) - **.opencode/skills/issue/SKILL.md:155-163** [docs] Документация устарела после этого PR. Строки 155-163 говорят: «Если label не существует в репо — tool упадёт. Создай через `curl` к Forgejo API...». Теперь tool **автосоздаёт** labels — эта инструкция вводит агентов в заблуждение: они будут делать лишний `curl POST /labels` перед вызовом tool (не ломает функциональность — label уже будет существовать в map — но лишняя работа). Рекомендуется обновить: заменить блок 155-163 на «Если label не существует — tool автосоздаст его с дефолтным цветом `#ededed`. Явное создание через curl нужно только если хочешь кастомный color». Fix: обновить `issue/SKILL.md:155-163` в отдельном PR (cosmetic docs, не блокирует merge). - **.opencode/tools/_shared.ts:119** [robustness] `GET /repos/${full}/labels` без `?page=` параметра возвращает только первую страницу (Forgejo default page size = 50). Если в репо >50 labels, label на странице 2+ не найдется в map → автосоздастся как **дубликат** (POST /labels создаст label с тем же name — Forgejo не запрещает дубликаты имён). Для репозиториев slaid098 (несколько labels) это unlikely, но для будущих масштабов стоит учесть. Fix (если понадобится): добавить пагинацию — цикл `?page=1&limit=50` пока ответ не пустой, или `?limit=10000` (Forgejo поддерживает большой limit). Не блокирует merge — текущие репо <10 labels. - **.opencode/tools/_shared.ts:127-146** [design] Auto-create без подтверждения: агент с опечаткой в label name (например `"bugg"` вместо `"bug"`) молча создаст мусорный label. Для single-user tool это приемлемо (YAGNI), но стоит задокументировать в `issue/SKILL.md` как known behavior. Не требует флага подтверждения — добавит сложность ради редкого кейса. ### Verdict: APPROVE
slaid098 deleted branch fix/create-issue/labels-int64 2026-08-07 13:19:48 +03:00
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
slaid098/opencode-config!17
No description provided.