From 8745cc342633e3eb155bce26e4fd361f39dc3f77 Mon Sep 17 00:00:00 2001 From: Harlan Wilton Date: Sat, 29 Aug 2026 14:15:21 +1000 Subject: [PATCH] fix(registry): read skills-raw branch from repos, not a mis-parsed ungh call ungh nests defaultBranch under .repo; the handler asserted it at the top level, so every request fell back to the 'main' branch. Repositories whose default branch is not main (theprimeagen/skills, vercel/next.js, fastapi/fastapi, ...) answered 503 on /api/skills-raw, which is the surface the run command fetches. The branch is already stored in repos.default_branch, the same source skill-files and skill-asset read. Read it there, drop the per-request ungh repo metadata round trip, and keep 'main' only as the fallback for rows with no recorded branch. --- .../server/api/skills-raw/[...slug].get.ts | 27 +++++++++------ test/unit/skills-raw-tree-outage.test.ts | 33 +++++++++++++++++-- 2 files changed, 47 insertions(+), 13 deletions(-) diff --git a/layers/registry/server/api/skills-raw/[...slug].get.ts b/layers/registry/server/api/skills-raw/[...slug].get.ts index 36b14cc7..7035b2b1 100644 --- a/layers/registry/server/api/skills-raw/[...slug].get.ts +++ b/layers/registry/server/api/skills-raw/[...slug].get.ts @@ -1,7 +1,7 @@ import { readCache, writeCache } from '#shared/server/cache' import { defineApiHandler } from '#shared/server/handler' import { normalizeSkillAssetFilePath } from '#shared/skill-asset-path' -import { resolveRepoSourceIdentity } from '../../utils/repo-source-identity' +import { resolveRepoSourceIdentityFromRow } from '../../utils/repo-source-identity' import { findSkill } from '../../utils/skills-registry' import { fetchUpstreamText } from '../../utils/upstream-text' @@ -16,6 +16,12 @@ interface RawCache { path: string | null } +interface SkillSourceRow { + default_branch: string | null + source_owner: string | null + source_repo: string | null +} + export default defineApiHandler({ handler: async ({ event, platform }) => { const slugParam = getRouterParam(event, 'slug') @@ -44,7 +50,16 @@ export default defineApiHandler({ if (!skill) throw createError({ statusCode: 404, message: 'Skill not found' }) - const source = await resolveRepoSourceIdentity(platform.db, skill) + const sourceRow = await platform.db + .prepare(` + SELECT r.default_branch, r.source_owner, r.source_repo + FROM skills s JOIN repos r ON r.owner = s.owner AND r.repo = s.repo + WHERE s.owner = ? AND s.repo = ? AND s.name = ? + `) + .bind(skill.owner, skill.repo, skill.name) + .first() + const source = resolveRepoSourceIdentityFromRow(skill, sourceRow ?? undefined) + const branch = sourceRow?.default_branch || 'main' const cacheKey = filePath ? `skills:raw:v2:${source.owner}/${source.repo}/${skill.name}:${filePath}` : `skills:raw:v2:${source.owner}/${source.repo}/${skill.name}` @@ -56,14 +71,6 @@ export default defineApiHandler({ return cached.body } - const repoMeta = await $fetch<{ defaultBranch?: string }>( - `https://ungh.cc/repos/${source.owner}/${source.repo}`, - ).catch(() => { - emitOperationalEvent(createWideEvent({ operation: 'skill-raw-repo-fetch', outcome: 'failed' })) - return null - }) - const branch = repoMeta?.defaultBranch || 'main' - const treeResult = await $fetch<{ files?: { path: string }[] }>( `https://ungh.cc/repos/${source.owner}/${source.repo}/files/${branch}`, ).then( diff --git a/test/unit/skills-raw-tree-outage.test.ts b/test/unit/skills-raw-tree-outage.test.ts index 0b5c8db3..421c7207 100644 --- a/test/unit/skills-raw-tree-outage.test.ts +++ b/test/unit/skills-raw-tree-outage.test.ts @@ -31,9 +31,6 @@ beforeEach(() => { vi.stubGlobal('setHeader', (_event: H3Event, name: string, value: string | number) => { responseHeaders.set(name, value) }) - vi.stubGlobal('$fetch', vi.fn() - .mockResolvedValueOnce({ defaultBranch: 'main' }) - .mockRejectedValueOnce(new Error('tree unavailable'))) }) afterEach(() => { @@ -43,6 +40,8 @@ afterEach(() => { describe('skills-raw tree outage', () => { it('returns a retryable error without caching the Skill as missing', async () => { + vi.stubGlobal('$fetch', vi.fn().mockRejectedValue(new Error('tree unavailable'))) + const handler = (await import('../../layers/registry/server/api/skills-raw/[...slug].get')).default await expect(handler(event())).rejects.toMatchObject({ statusCode: 503 }) @@ -52,6 +51,34 @@ describe('skills-raw tree outage', () => { }) }) +describe('skills-raw default branch', () => { + it('serves a skill whose repository default branch is not main, without a repo metadata round trip', async () => { + fixture.raw.prepare(`UPDATE repos SET default_branch = 'master' WHERE owner = 'owner' AND repo = 'repo'`).run() + // ungh's real wire shape: defaultBranch lives under .repo, and a tree + // listing only exists for the repository's actual default branch. + const fetchMock = vi.fn((url: string) => { + if (url.endsWith('/files/master')) + return Promise.resolve({ files: [{ path: 'skills/skill/SKILL.md' }] }) + if (url.endsWith('/files/main')) + return Promise.reject(new Error('404 Not Found')) + return Promise.resolve({ repo: { defaultBranch: 'master' } }) + }) + vi.stubGlobal('$fetch', fetchMock) + vi.stubGlobal('fetch', vi.fn().mockResolvedValue( + new Response('# Skill body', { status: 200 }), + )) + + const handler = (await import('../../layers/registry/server/api/skills-raw/[...slug].get')).default + const body = await handler(event()) + + expect(body).toBe('# Skill body') + expect(responseHeaders.get('x-skilld-source')).toBe('owner/repo@master/skills/skill/SKILL.md') + const treeUrls = fetchMock.mock.calls.map(([url]) => String(url)) + expect(treeUrls).toEqual(['https://ungh.cc/repos/owner/repo/files/master']) + expect(treeUrls.some(url => url.endsWith('/files/main'))).toBe(false) + }) +}) + function event(): H3Event { return { method: 'GET',