From 710e00e02d0c542240c8e8b1bf499aa6a1c3a50b Mon Sep 17 00:00:00 2001 From: Jacob MacDonald Date: Mon, 22 Sep 2025 12:12:41 -0700 Subject: [PATCH] use github release artifacts instead of cloning repos when available (#9147) --- packages/cli/src/config/extensions/github.ts | 63 +++++++++++++------- 1 file changed, 40 insertions(+), 23 deletions(-) diff --git a/packages/cli/src/config/extensions/github.ts b/packages/cli/src/config/extensions/github.ts index 8d83fe40e4..f1e9a65bc4 100644 --- a/packages/cli/src/config/extensions/github.ts +++ b/packages/cli/src/config/extensions/github.ts @@ -103,11 +103,11 @@ export function parseGitHubRepoForReleases(source: string): { return { owner, repo }; } -async function fetchFromGithub( +async function fetchReleaseFromGithub( owner: string, repo: string, ref?: string, -): Promise<{ assets: Asset[]; tag_name: string }> { +): Promise { const endpoint = ref ? `releases/tags/${ref}` : 'releases/latest'; const url = `https://api.github.com/repos/${owner}/${repo}/${endpoint}`; return await fetchJson(url); @@ -199,7 +199,7 @@ export async function checkForExtensionUpdate( } const { owner, repo } = parseGitHubRepoForReleases(source); - const releaseData = await fetchFromGithub( + const releaseData = await fetchReleaseFromGithub( owner, repo, installMetadata.ref, @@ -228,7 +228,7 @@ export async function downloadFromGitHubRelease( const { owner, repo } = parseGitHubRepoForReleases(source); try { - const releaseData = await fetchFromGithub(owner, repo, ref); + const releaseData = await fetchReleaseFromGithub(owner, repo, ref); if (!releaseData) { throw new Error( `No release data found for ${owner}/${repo} at tag ${ref}`, @@ -236,24 +236,36 @@ export async function downloadFromGitHubRelease( } const asset = findReleaseAsset(releaseData.assets); - if (!asset) { - // If there are no release assets, then we just clone the repo using the - // ref the release points to. - await cloneFromGit( - { - ...installMetadata, - ref: releaseData.tag_name, - }, - destination, + let archiveUrl: string | undefined; + let isTar = false; + let isZip = false; + if (asset) { + archiveUrl = asset.browser_download_url; + } else { + if (releaseData.tarball_url) { + archiveUrl = releaseData.tarball_url; + isTar = true; + } else if (releaseData.zipball_url) { + archiveUrl = releaseData.zipball_url; + isZip = true; + } + } + if (!archiveUrl) { + throw new Error( + `No assets found for release with tag ${releaseData.tag_name}`, ); - return releaseData.tag_name; + } + let downloadedAssetPath = path.join( + destination, + path.basename(new URL(archiveUrl).pathname), + ); + if (isTar && !downloadedAssetPath.endsWith('.tar.gz')) { + downloadedAssetPath += '.tar.gz'; + } else if (isZip && !downloadedAssetPath.endsWith('.zip')) { + downloadedAssetPath += '.zip'; } - const downloadedAssetPath = path.join( - destination, - path.basename(asset.browser_download_url), - ); - await downloadFile(asset.browser_download_url, downloadedAssetPath); + await downloadFile(archiveUrl, downloadedAssetPath); extractFile(downloadedAssetPath, destination); @@ -284,6 +296,13 @@ export async function downloadFromGitHubRelease( } } +interface GithubReleaseData { + assets: Asset[]; + tag_name: string; + tarball_url?: string; + zipball_url?: string; +} + interface Asset { name: string; browser_download_url: string; @@ -326,9 +345,7 @@ export function findReleaseAsset(assets: Asset[]): Asset | undefined { return undefined; } -async function fetchJson( - url: string, -): Promise<{ assets: Asset[]; tag_name: string }> { +async function fetchJson(url: string): Promise { const headers: { 'User-Agent': string; Authorization?: string } = { 'User-Agent': 'gemini-cli', }; @@ -348,7 +365,7 @@ async function fetchJson( res.on('data', (chunk) => chunks.push(chunk)); res.on('end', () => { const data = Buffer.concat(chunks).toString(); - resolve(JSON.parse(data) as { assets: Asset[]; tag_name: string }); + resolve(JSON.parse(data) as T); }); }) .on('error', reject);