From 650230734032872bb42c45919b546b2f8d9d9d0a Mon Sep 17 00:00:00 2001 From: Arnaud Date: Sun, 19 Jul 2026 11:40:52 +0200 Subject: [PATCH] fix: resolve Gitea label names to IDs before creating an issue (v0.51.5) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Gitea's POST /repos/{repo}/issues expects `labels` as an array of numeric label IDs, not name strings — every issue creation was failing with a 422 ("cannot unmarshal JSON string into Go int64"). Now fetches the repo's label list and resolves "bug"/"enhancement"/"question" to IDs by name first; a repo with no matching labels (or a failed lookup) just creates the issue unlabeled instead of failing the whole request. Co-Authored-By: Claude Sonnet 5 --- CHANGELOG.md | 5 +++++ apps/web/lib/changelog.ts | 9 ++++++++- apps/web/lib/gitea.ts | 34 +++++++++++++++++++++++++++++----- apps/web/package.json | 2 +- package.json | 2 +- 5 files changed, 44 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 954c711..1dfc223 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,11 @@ All notable changes to Epicure are documented here. This file is mirrored in-app at `/changelog` (and in the admin dashboard) via `apps/web/lib/changelog.ts` — update both together. +## 0.51.5 — 2026-07-19 13:05 + +### Fixed +- Gitea issue creation was sending label names ("bug", "enhancement") where Gitea's API expects numeric label IDs, failing with a 422 on every attempt. Now looks up the repo's actual label IDs by name first — issues still get created (just unlabeled) if the repo doesn't have matching labels yet. + ## 0.51.4 — 2026-07-19 12:50 ### Fixed diff --git a/apps/web/lib/changelog.ts b/apps/web/lib/changelog.ts index 7b04e30..82dfa5c 100644 --- a/apps/web/lib/changelog.ts +++ b/apps/web/lib/changelog.ts @@ -1,5 +1,5 @@ // Mirrors CHANGELOG.md at the repo root — update both together. -export const APP_VERSION = "0.51.4"; +export const APP_VERSION = "0.51.5"; export type ChangelogEntry = { version: string; @@ -11,6 +11,13 @@ export type ChangelogEntry = { }; export const CHANGELOG: ChangelogEntry[] = [ + { + version: "0.51.5", + date: "2026-07-19 13:05", + fixed: [ + "Gitea issue creation was sending label names (\"bug\", \"enhancement\") where Gitea's API expects numeric label IDs, failing with a 422 on every attempt. Now looks up the repo's actual label IDs by name first — issues still get created (just unlabeled) if the repo doesn't have matching labels yet.", + ], + }, { version: "0.51.4", date: "2026-07-19 12:50", diff --git a/apps/web/lib/gitea.ts b/apps/web/lib/gitea.ts index 94f7bf5..7746872 100644 --- a/apps/web/lib/gitea.ts +++ b/apps/web/lib/gitea.ts @@ -1,11 +1,31 @@ import { getSiteSetting } from "@/lib/site-settings"; -const LABELS: Record = { +const LABEL_NAMES: Record = { bug: ["bug"], suggestion: ["enhancement"], question: ["question"], }; +/** Gitea's issue-create endpoint takes label IDs, not names — resolve by + * fetching the repo's existing labels and matching case-insensitively. + * A repo with no matching labels (or a lookup failure) just means the + * issue is created unlabeled, not that creation fails outright. */ +async function resolveLabelIds(baseUrl: string, token: string, repo: string, names: string[]): Promise { + if (names.length === 0) return []; + try { + const res = await fetch(`${baseUrl}/api/v1/repos/${repo}/labels`, { + headers: { Authorization: `token ${token}` }, + signal: AbortSignal.timeout(10000), + }); + if (!res.ok) return []; + const labels = (await res.json()) as { id: number; name: string }[]; + const wanted = new Set(names.map((n) => n.toLowerCase())); + return labels.filter((l) => wanted.has(l.name.toLowerCase())).map((l) => l.id); + } catch { + return []; + } +} + /** * Opens an issue on the configured Gitea repo for a support ticket. Returns * the issue's HTML URL on success, or null if Gitea isn't configured or the @@ -17,18 +37,22 @@ export async function createGiteaIssue(opts: { title: string; body: string; }): Promise<{ url: string | null; error: string | null }> { - const [baseUrl, token, repo] = await Promise.all([ + const [rawBaseUrl, token, repo] = await Promise.all([ getSiteSetting("GITEA_URL"), getSiteSetting("GITEA_TOKEN"), getSiteSetting("GITEA_REPO"), ]); - if (!baseUrl || !token || !repo) { + if (!rawBaseUrl || !token || !repo) { return { url: null, error: null }; } + const baseUrl = rawBaseUrl.replace(/\/$/, ""); + try { - const res = await fetch(`${baseUrl.replace(/\/$/, "")}/api/v1/repos/${repo}/issues`, { + const labelIds = await resolveLabelIds(baseUrl, token, repo, LABEL_NAMES[opts.type] ?? []); + + const res = await fetch(`${baseUrl}/api/v1/repos/${repo}/issues`, { method: "POST", headers: { "Content-Type": "application/json", @@ -37,7 +61,7 @@ export async function createGiteaIssue(opts: { body: JSON.stringify({ title: opts.title, body: opts.body, - labels: LABELS[opts.type] ?? [], + labels: labelIds, }), signal: AbortSignal.timeout(10000), }); diff --git a/apps/web/package.json b/apps/web/package.json index c91946b..c212464 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -1,6 +1,6 @@ { "name": "@epicure/web", - "version": "0.51.4", + "version": "0.51.5", "private": true, "scripts": { "dev": "next dev", diff --git a/package.json b/package.json index 81de343..cfcfa2b 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "epicure", - "version": "0.51.4", + "version": "0.51.5", "private": true, "scripts": { "dev": "pnpm --filter web dev",