import { Error2, ErrorCodes } from '#/errors'; import type { GithubRef } from './source'; import type { PluginGithubRef } from './types'; export interface GithubSourceInput { readonly kind: 'github'; readonly owner: string; readonly repo: string; readonly ref?: GithubRef; } export interface GithubSourceResolution { readonly tarballUrl: string; readonly displayVersion: string; readonly ref: PluginGithubRef; } export async function resolveGithubCommitSha( owner: string, repo: string, ref: string, ): Promise { const encodedRef = encodeCodeloadRefPath(ref); const url = `https://github.com/${owner}/${repo}/commits/${encodedRef}.atom`; const resp = await fetch(url, { headers: { accept: 'application/atom+xml', range: 'bytes=0-4095' }, signal: AbortSignal.timeout(10_000), }); if (!resp.ok) { throw new Error2( ErrorCodes.PLUGIN_LOAD_FAILED, `Could not resolve ${owner}/${repo}@${ref}: HTTP ${resp.status} ${resp.statusText}.`, { details: { owner, repo, ref, status: resp.status } }, ); } const feed = await resp.text(); const sha = /Grit::Commit\/([0-9a-f]{40})/i.exec(feed)?.[1]; if (sha === undefined) { throw new Error2( ErrorCodes.PLUGIN_LOAD_FAILED, `Could not resolve ${owner}/${repo}@${ref} to a commit SHA.`, { details: { owner, repo, ref } }, ); } return sha.toLowerCase(); } export async function resolveGithubSource( input: GithubSourceInput, ): Promise { const { owner, repo } = input; if (input.ref !== undefined) { return { tarballUrl: codeloadUrl(owner, repo, input.ref), displayVersion: input.ref.value, ref: { kind: input.ref.kind, value: input.ref.value }, }; } const latestTag = await tryResolveLatestReleaseTag(owner, repo); if (latestTag !== undefined) { return { tarballUrl: codeloadUrl(owner, repo, { kind: 'tag', value: latestTag }), displayVersion: latestTag, ref: { kind: 'tag', value: latestTag }, }; } const headProbe = await fetch(`https://codeload.github.com/${owner}/${repo}/zip/HEAD`, { method: 'HEAD', signal: AbortSignal.timeout(10_000), }); if (headProbe.status === 404) { throw new Error2( ErrorCodes.PLUGIN_LOAD_FAILED, `Repository \`${owner}/${repo}\` not found or not accessible.`, { details: { owner, repo } }, ); } if (!headProbe.ok) { throw new Error2( ErrorCodes.PLUGIN_LOAD_FAILED, `Could not access \`${owner}/${repo}\`: HTTP ${headProbe.status} ${headProbe.statusText}.`, { details: { owner, repo, status: headProbe.status } }, ); } return { tarballUrl: `https://codeload.github.com/${owner}/${repo}/zip/HEAD`, displayVersion: 'HEAD', ref: { kind: 'branch', value: 'HEAD' }, }; } async function tryResolveLatestReleaseTag(owner: string, repo: string): Promise { const url = `https://github.com/${owner}/${repo}/releases/latest`; const resp = await fetch(url, { redirect: 'manual', signal: AbortSignal.timeout(10_000), }); if (resp.status === 404) return undefined; if (resp.status !== 301 && resp.status !== 302) { throw new Error2( ErrorCodes.PLUGIN_LOAD_FAILED, `Could not look up latest release of \`${owner}/${repo}\`: ` + `HTTP ${resp.status} ${resp.statusText} (${url}). ` + `Pin a specific ref with \`/tree/\` to bypass release lookup.`, { details: { owner, repo, status: resp.status, url } }, ); } const location = resp.headers.get('location'); if (location === null) return undefined; const match = /\/releases\/tag\/([^/?#]+)/.exec(location); if (match === null) return undefined; try { return decodeURIComponent(match[1]!); } catch { return match[1]; } } function codeloadUrl(owner: string, repo: string, ref: GithubRef): string { const base = `https://codeload.github.com/${owner}/${repo}/zip`; const encoded = encodeCodeloadRefPath(ref.value); if (ref.kind === 'sha') return `${base}/${encoded}`; if (ref.kind === 'tag') return `${base}/refs/tags/${encoded}`; return `${base}/${encoded}`; } function encodeCodeloadRefPath(value: string): string { return value.split('/').map(encodeURIComponent).join('/'); }