fix(tools): create-issue sends labels as strings, Forgejo API expects int64 IDs (HTTP 422) #4

Open
opened 2026-08-06 20:56:42 +03:00 by slaid098 · 0 comments
Owner

Контекст

При создании issue через create-issue tool в Forgejo-режиме (FORGEJO_URL задан в окружении) tool падает с HTTP 422 Unprocessable Entity, если передан параметр labels.

Воспроизведение: вызов create-issue с labels: ["enhancement"] → Forgejo API возвращает 422. Повторный вызов без labels — успешен (issue создается без меток).

Причина: tool отправляет массив labels как массив строк (["enhancement"]), но Forgejo API endpoint POST /api/v1/repos/{owner}/{repo}/issues ожидает в CreateIssueOption.labels массив int64 ID лейблов (не имён). См. https://codeberg.org/forgejo/forgejo/src/branch/forgejo/modules/structs/issue.goLabels []int64.

GitHub API (POST /repos/{owner}/{repo}/issues) принимает строки — поэтому в GitHub-режиме tool работает корректно. Расхождение контрактов GitHub vs Forgejo.

Обход, который использовал subagent: повторный вызов без labels, затем прикрепление label через отдельный вызов API POST /api/v1/repos/{repo}/issues/{number}/labels с правильным форматом. Но это ручной workaround, не решение.

Задача

Починить create-issue tool для корректной работы с labels в Forgejo-режиме: принимать имена labels как строки (текущий контракт tool'а), внутри конвертировать в int64 ID через lookup GET /api/v1/repos/{repo}/labels перед отправкой POST .../issues. Если label не найден — либо создать его (POST .../labels), либо вернуть понятную ошибку (решить при имплементации).

Контракты

  • Tool контракт (внешний, для агентов): create-issue(title, body, labels: string[], repo?) — labels как массив строк имён, не ID. Этот контракт НЕ меняем (обратная совместимость с GitHub-режимом и существующими вызовами).
  • Forgejo API POST /api/v1/repos/{owner}/{repo}/issues: тело {"title": "...", "body": "...", "labels": [int64, ...]}. Документация: https://forgejo.org/docs/api/ (swagger /api/v1).
  • Forgejo API GET /api/v1/repos/{owner}/{repo}/labels: список лейблов с id/name.
  • Forgejo API POST /api/v1/repos/{owner}/{repo}/labels: создание лейбла (если решено автосоздавать).
  • Forgejo API POST /api/v1/repos/{owner}/{repo}/issues/{number}/labels: альтернативный путь — создать issue без labels, потом прикрепить через этот endpoint (принимает {"labels": ["name1", "name2"]} — строки, не ID!). См. https://forgejo.org/docs/api/IssueLabelsOption.labels это []string.

Инварианты

  • Tool контракт снаружи НЕ меняется — labels остаются string[] имён
  • GitHub-режим (fallback на gh CLI) не должен сломаться — gh issue create --label "enhancement" принимает строки
  • Поведение при отсутствии label на Forgejo: либо создать (с дефолтным цветом), либо ошибиться с понятным сообщением — решение за имплементатором, но должно быть задокументировано в коде
  • Идемпотентность: повторное создание issue с тем же label не должно дублировать label на issue

Граничные случаи

  • Label с именем, не существующим на репо: auto-create vs error — выбрать при имплементации
  • Label с пробелами/спецсимволами в имени: Forgejo принимает любые строки, но lookup должен матчить по точному имени (case-sensitive?)
  • Пустой массив labels: должен работать как сейчас (не отправлять поле labels в теле)
  • Mix существующих и несуществующих labels: частичный успех или all-or-nothing?
  • Label names with colons (например "type:bug") — Forgejo принимает, но в URL encode/decode нужно быть аккуратным
  • При использовании альтернативного пути (создать issue без labels, потом POST labels со строками) — если падает второй вызов, issue остаётся без labels (частичный успех). Atomicity.
  • Rate limits: два вызова вместо одного — вдвое больше запросов, учесть при schedule

Влияние на связанные компоненты

  • create-issue.ts — основной файл правки,Forgejo-ветка (через _shared.ts:callForgejoGh или inline). GitHub-ветка (runGh) не трогаем
  • _shared.ts — если lookup/conversion вынесен в shared helper, может потребоваться расширение
  • create-pr.ts — потенциально тот же баг с labels (PR тоже принимают labels). Проверить и при необходимости починить одновременно
  • post-review.ts — не использует labels
  • merge-pr.ts — не использует labels
  • Другие инструменты через runGh — проверить кто еще передаёт labels в API-вызовы
  • Документация tool'а в opencode.json description — если упоминается labels format, обновить

Вне scope

  • Изменение внешнего контракта tool'а (labels остаются строками)
  • Изменение GitHub-режима (fallback на gh CLI)
  • Аналогичный баг в create-pr если он есть — сделать отдельной задачей или включить в эту на усмотрение имплементатора, но не блокировать
  • Глобальный рефакторинг _shared.ts

Критерии приемки

  • Воспроизведён баг: вызов create-issue с labels: ["enhancement"] на Forgejo-репо — до фикса возвращает 422, после фикса успешно создаёт issue с label
  • Lookup существующих labels через GET /api/v1/repos/{repo}/labels — реализован и используется
  • Несуществующий label: либо автосоздаётся, либо возвращает понятную ошибку с именем недостающего label
  • Пустой массив labels: работает (поле не отправляется, issue создаётся без labels)
  • GitHub-режим (без FORGEJO_URL): не сломан, gh issue create --label ... отрабатывает как раньше
  • Проверено что create-pr.ts с labels в Forgejo-режиме — либо починен в этом PR, либо заведён отдельный issue если баг есть
  • Smoke-test: создать тестовый issue с labels через tool на Forgejo-репо → убедиться что label прикреплён (GET /issues/{n} вернёт labels массив не пустой)
## Контекст При создании issue через `create-issue` tool в Forgejo-режиме (`FORGEJO_URL` задан в окружении) tool падает с HTTP 422 Unprocessable Entity, если передан параметр `labels`. Воспроизведение: вызов `create-issue` с `labels: ["enhancement"]` → Forgejo API возвращает 422. Повторный вызов без `labels` — успешен (issue создается без меток). Причина: tool отправляет массив labels как массив строк (`["enhancement"]`), но Forgejo API endpoint `POST /api/v1/repos/{owner}/{repo}/issues` ожидает в `CreateIssueOption.labels` массив int64 ID лейблов (не имён). См. https://codeberg.org/forgejo/forgejo/src/branch/forgejo/modules/structs/issue.go — `Labels []int64`. GitHub API (`POST /repos/{owner}/{repo}/issues`) принимает строки — поэтому в GitHub-режиме tool работает корректно. Расхождение контрактов GitHub vs Forgejo. Обход, который использовал subagent: повторный вызов без labels, затем прикрепление label через отдельный вызов API `POST /api/v1/repos/{repo}/issues/{number}/labels` с правильным форматом. Но это ручной workaround, не решение. ## Задача Починить `create-issue` tool для корректной работы с labels в Forgejo-режиме: принимать имена labels как строки (текущий контракт tool'а), внутри конвертировать в int64 ID через lookup `GET /api/v1/repos/{repo}/labels` перед отправкой `POST .../issues`. Если label не найден — либо создать его (`POST .../labels`), либо вернуть понятную ошибку (решить при имплементации). ## Контракты - Tool контракт (внешний, для агентов): `create-issue(title, body, labels: string[], repo?)` — labels как массив строк имён, не ID. Этот контракт НЕ меняем (обратная совместимость с GitHub-режимом и существующими вызовами). - Forgejo API `POST /api/v1/repos/{owner}/{repo}/issues`: тело `{"title": "...", "body": "...", "labels": [int64, ...]}`. Документация: https://forgejo.org/docs/api/ (swagger /api/v1). - Forgejo API `GET /api/v1/repos/{owner}/{repo}/labels`: список лейблов с id/name. - Forgejo API `POST /api/v1/repos/{owner}/{repo}/labels`: создание лейбла (если решено автосоздавать). - Forgejo API `POST /api/v1/repos/{owner}/{repo}/issues/{number}/labels`: альтернативный путь — создать issue без labels, потом прикрепить через этот endpoint (принимает `{"labels": ["name1", "name2"]}` — строки, не ID!). См. https://forgejo.org/docs/api/ — `IssueLabelsOption.labels` это `[]string`. ## Инварианты - Tool контракт снаружи НЕ меняется — labels остаются `string[]` имён - GitHub-режим (fallback на `gh` CLI) не должен сломаться — `gh issue create --label "enhancement"` принимает строки - Поведение при отсутствии label на Forgejo: либо создать (с дефолтным цветом), либо ошибиться с понятным сообщением — решение за имплементатором, но должно быть задокументировано в коде - Идемпотентность: повторное создание issue с тем же label не должно дублировать label на issue ## Граничные случаи - Label с именем, не существующим на репо: auto-create vs error — выбрать при имплементации - Label с пробелами/спецсимволами в имени: Forgejo принимает любые строки, но lookup должен матчить по точному имени (case-sensitive?) - Пустой массив labels: должен работать как сейчас (не отправлять поле `labels` в теле) - Mix существующих и несуществующих labels: частичный успех или all-or-nothing? - Label names with colons (например "type:bug") — Forgejo принимает, но в URL encode/decode нужно быть аккуратным - При использовании альтернативного пути (создать issue без labels, потом POST labels со строками) — если падает второй вызов, issue остаётся без labels (частичный успех). Atomicity. - Rate limits: два вызова вместо одного — вдвое больше запросов, учесть при schedule ## Влияние на связанные компоненты - `create-issue.ts` — основной файл правки,Forgejo-ветка (через `_shared.ts:callForgejoGh` или inline). GitHub-ветка (`runGh`) не трогаем - `_shared.ts` — если lookup/conversion вынесен в shared helper, может потребоваться расширение - `create-pr.ts` — потенциально тот же баг с labels (PR тоже принимают labels). Проверить и при необходимости починить одновременно - `post-review.ts` — не использует labels - `merge-pr.ts` — не использует labels - Другие инструменты через `runGh` — проверить кто еще передаёт labels в API-вызовы - Документация tool'а в `opencode.json` description — если упоминается labels format, обновить ## Вне scope - Изменение внешнего контракта tool'а (labels остаются строками) - Изменение GitHub-режима (fallback на `gh` CLI) - Аналогичный баг в `create-pr` если он есть — сделать отдельной задачей или включить в эту на усмотрение имплементатора, но не блокировать - Глобальный рефакторинг `_shared.ts` ## Критерии приемки - [ ] Воспроизведён баг: вызов `create-issue` с `labels: ["enhancement"]` на Forgejo-репо — до фикса возвращает 422, после фикса успешно создаёт issue с label - [ ] Lookup существующих labels через `GET /api/v1/repos/{repo}/labels` — реализован и используется - [ ] Несуществующий label: либо автосоздаётся, либо возвращает понятную ошибку с именем недостающего label - [ ] Пустой массив labels: работает (поле не отправляется, issue создаётся без labels) - [ ] GitHub-режим (без `FORGEJO_URL`): не сломан, `gh issue create --label ...` отрабатывает как раньше - [ ] Проверено что `create-pr.ts` с labels в Forgejo-режиме — либо починен в этом PR, либо заведён отдельный issue если баг есть - [ ] Smoke-test: создать тестовый issue с labels через tool на Forgejo-репо → убедиться что label прикреплён (GET `/issues/{n}` вернёт `labels` массив не пустой)
Sign in to join this conversation.
No labels
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#4
No description provided.