diff --git a/src/lib/server/integrations/komodo.ts b/src/lib/server/integrations/komodo.ts index 6e5ed49..a192287 100644 --- a/src/lib/server/integrations/komodo.ts +++ b/src/lib/server/integrations/komodo.ts @@ -548,27 +548,55 @@ function unwrapTagList(body: unknown): unknown[] { return []; } +/** + * Fetch the tag list from Komodo. Two request styles are attempted in order: + * + * 1. `POST /read/ListTags` with an empty body — the per-endpoint style + * already used by the rest of this file (see execute/RestartDeployment). + * 2. `POST /read` with `{type: "ListTags", params: {}}` — the tagged-union + * style used by mbecker20/komodo's canonical API. If your Komodo build + * wires up only the tagged-union path, style #1 returns something we + * can't parse (empty array or non-tag payload) and we fall through. + * + * The winning parsed list is returned. The raw response bodies get logged + * only when both attempts fail to yield a match, so a fresh Komodo API shape + * is diagnosable without spamming stderr on the happy path. + */ +async function fetchTagsWithFallback(): Promise<{ tags: unknown[]; rawBodies: string[] }> { + const attempts: Array<{ path: string; body: unknown }> = [ + { path: 'read/ListTags', body: {} }, + { path: 'read', body: { type: 'ListTags', params: {} } } + ]; + const rawBodies: string[] = []; + for (const attempt of attempts) { + let res: Response; + try { + res = await komodoCall(attempt.path, attempt.body); + } catch (err) { + rawBodies.push(`${attempt.path}: network error — ${(err as Error).message}`); + continue; + } + if (!res.ok) { + const snippet = await res.text().catch(() => ''); + rawBodies.push(`${attempt.path}: HTTP ${res.status}${snippet ? ` — ${truncate(snippet)}` : ''}`); + continue; + } + const raw = await res.text().catch(() => ''); + rawBodies.push(`${attempt.path}: ${truncate(raw)}`); + let parsed: unknown; + try { + parsed = JSON.parse(raw); + } catch { + continue; + } + const tags = unwrapTagList(parsed); + if (tags.length > 0) return { tags, rawBodies }; + } + return { tags: [], rawBodies }; +} + async function findTagByName(name: string): Promise { - let listRes: Response; - try { - listRes = await komodoCall('read/ListTags', {}); - } catch (err) { - console.warn(`[komodoEnsureTag] ListTags network failure for "${name}":`, (err as Error).message); - return null; - } - if (!listRes.ok) { - const snippet = await listRes.text().catch(() => ''); - console.warn(`[komodoEnsureTag] ListTags HTTP ${listRes.status} for "${name}"${snippet ? `: ${truncate(snippet)}` : ''}`); - return null; - } - let body: unknown; - try { - body = await listRes.json(); - } catch (err) { - console.warn(`[komodoEnsureTag] ListTags non-JSON body for "${name}":`, (err as Error).message); - return null; - } - const tags = unwrapTagList(body); + const { tags, rawBodies } = await fetchTagsWithFallback(); for (const t of tags) { if (!t || typeof t !== 'object') continue; const tagName = (t as { name?: unknown }).name; @@ -576,6 +604,11 @@ async function findTagByName(name: string): Promise { const id = extractTagId(t); if (id) return { ok: true, id }; } + // Miss — dump the raw bodies so the next iteration knows the real shape. + console.warn( + `[komodoEnsureTag] no matchable tag "${name}" in any ListTags variant. Bodies:\n ` + + rawBodies.join('\n ') + ); return null; }