fix: resolve Gitea label names to IDs before creating an issue (v0.51.5)
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 <noreply@anthropic.com>
This commit is contained in:
@@ -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.
|
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
|
## 0.51.4 — 2026-07-19 12:50
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
// Mirrors CHANGELOG.md at the repo root — update both together.
|
// 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 = {
|
export type ChangelogEntry = {
|
||||||
version: string;
|
version: string;
|
||||||
@@ -11,6 +11,13 @@ export type ChangelogEntry = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const CHANGELOG: 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",
|
version: "0.51.4",
|
||||||
date: "2026-07-19 12:50",
|
date: "2026-07-19 12:50",
|
||||||
|
|||||||
+29
-5
@@ -1,11 +1,31 @@
|
|||||||
import { getSiteSetting } from "@/lib/site-settings";
|
import { getSiteSetting } from "@/lib/site-settings";
|
||||||
|
|
||||||
const LABELS: Record<string, string[]> = {
|
const LABEL_NAMES: Record<string, string[]> = {
|
||||||
bug: ["bug"],
|
bug: ["bug"],
|
||||||
suggestion: ["enhancement"],
|
suggestion: ["enhancement"],
|
||||||
question: ["question"],
|
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<number[]> {
|
||||||
|
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
|
* 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
|
* 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;
|
title: string;
|
||||||
body: string;
|
body: string;
|
||||||
}): Promise<{ url: string | null; error: string | null }> {
|
}): 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_URL"),
|
||||||
getSiteSetting("GITEA_TOKEN"),
|
getSiteSetting("GITEA_TOKEN"),
|
||||||
getSiteSetting("GITEA_REPO"),
|
getSiteSetting("GITEA_REPO"),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
if (!baseUrl || !token || !repo) {
|
if (!rawBaseUrl || !token || !repo) {
|
||||||
return { url: null, error: null };
|
return { url: null, error: null };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const baseUrl = rawBaseUrl.replace(/\/$/, "");
|
||||||
|
|
||||||
try {
|
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",
|
method: "POST",
|
||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
@@ -37,7 +61,7 @@ export async function createGiteaIssue(opts: {
|
|||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
title: opts.title,
|
title: opts.title,
|
||||||
body: opts.body,
|
body: opts.body,
|
||||||
labels: LABELS[opts.type] ?? [],
|
labels: labelIds,
|
||||||
}),
|
}),
|
||||||
signal: AbortSignal.timeout(10000),
|
signal: AbortSignal.timeout(10000),
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@epicure/web",
|
"name": "@epicure/web",
|
||||||
"version": "0.51.4",
|
"version": "0.51.5",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "next dev",
|
"dev": "next dev",
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "epicure",
|
"name": "epicure",
|
||||||
"version": "0.51.4",
|
"version": "0.51.5",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "pnpm --filter web dev",
|
"dev": "pnpm --filter web dev",
|
||||||
|
|||||||
Reference in New Issue
Block a user